算法相关数据结构总结:
一、栈和队列
队列是先进先出,栈是先进后出。
1. 栈
(1)栈的基础知识
Java Stack类,栈是Vector的一个子类,实现后进先出的栈。
Stack stackA = new Stack();
栈是一种 “特殊” 的线性存储结构,因此栈的具体实现有以下两种方式:
- 顺序栈:采用顺序存储结构可以模拟栈存储数据的特点,从而实现栈存储结构;
- 链栈:采用链式存储结构实现栈结构;
注意:
Java中用来表达栈的功能(push/pop/peek
),更适用的是使用双端队列接口Deque
,并用实现类ArrayDeque/LinkedList
来进行初始化。
Deque<Integer> stack = new ArrayDeque<>();
Deque<Integer> stack = new LinkedList<>();
不用Stack至少有以下两点原因:
- 从
性能
上来说应该使用Deque代替Stack。Stack和Vector都是线程安全的,其实多数情况下并不需要做到线程安全,因此没有必要使用Stack。毕竟保证线程安全需要上锁,有额外的系统开销。 - Stack从Vector继承是个历史遗留问题,JDK官方已建议优先使用Deque的实现类来代替Stack。Stack从Vector继承的一个副作用是,
暴露了set/get方法,可以进行随机位置的访问
,这与Stack只能从尾巴上进行增减的本意相悖。
相比LinkedList,ArrayDeque会略胜一筹,不过差别通常可以忽略。建议使用ArrayDeque
。
ArrayDeque的方法
:
// 添加元素
addFirst() 在数组前面添加元素
addLast() 在数组后面添加元素
offerFirst() 在数组前面添加元素,并返回是否添加成功
offerLast(E e) 在数组后天添加元素,并返回是否添加成功
// 删除元素
removeFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将抛出异常
pollFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将返回null
removeLast()删除最后一个元素,并返回删除元素的值,如果为null,将抛出异常
pollLast()删除最后一个元素,并返回删除元素的值,如果为null,将返回null
removeFirstOccurrence(Object o) 删除第一次出现的指定元素
removeLastOccurrence(Object o) 删除最后一次出现的指定元素
// 获取元素
getFirst() 获取第一个元素,如果没有将抛出异常
getLast() 获取最后一个元素,如果没有将抛出异常
// 队列操作
add(E e) 在队列尾部添加一个元素
offer(E e) 在队列尾部添加一个元素,并返回是否成功
remove() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将抛出异常(其实底层调用的是removeFirst())
poll() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将返回null(其实调用的是pollFirst())
element() 获取第一个元素,如果没有将抛出异常
peek() 获取第一个元素,如果返回null
// 栈操作
push(E e) 栈顶添加一个元素
pop(E e) 移除栈顶元素,如果栈顶没有元素将抛出异常
// 其他
size() 获取队列中元素个数
isEmpty() 判断队列是否为空
iterator() 迭代器,从前向后迭代
descendingIterator() 迭代器,从后向前迭代
contain(Object o) 判断队列中是否存在该元素
toArray() 转成数组
clear() 清空队列
clone() 克隆(复制)一个新的队列
(2)栈的函数
Stack<Integer> stack = new Stack<Integer>();//建栈
stack.push(Element);//进栈
stack.pop();//出栈
stack.peek();//取栈顶值(不出栈)
stack.isEmpty();//判断栈是否为空
2. 队列
(1)队列的基础知识
LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。
Queue<String> queue = new LinkedList<String>();
队列存储结构的实现有以下两种方式:
- 顺序队列:在顺序表的基础上实现的队列结构;
- 链队列:在链表的基础上实现的队列结构;
(2)队列的函数
添加:queue.offer() queue.add()
删除队列第一个元素:queue.poll()返回null queue.remove()返回异常
查询队列头部元素:peek()返回null element()返回异常
(3)优先队列
PriorityQueue,即优先队列。优先队列的作用是能保证每次取出的元素都是队列中权值最小的。堆保证每次插入都排好序。
Java中PriorityQueue默认是小顶堆
,可以通过传入自定义的Comparator函数来实现大顶堆。
PriorityQueue<Integer> queue = new PriorityQueue<>()
大顶堆的写法:
匿名内部类型的写法:
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>(){
public int compare(Integer n1, Integer n2){
return n2-n1;
}
});
lamdba表达式的写法:
PriorityQueue<Integer> queue = new PriorityQueue<>((x,y) -> (y-x));
优先队列的方法:
创建:PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>()
add()和offer() 向优先队列中插入元素
element()和peek() 获取但不删除队首元素
remove()和poll() 获取并删除队首元素
二、leetcode例题讲解栈和队列问题
1. 基础题目
232. 用栈实现队列
leetcode题目链接:232. 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
- void push(int x) 将元素 x 推到队列的末尾
- int pop() 从队列的开头移除并返回元素
- int peek() 返回队列开头的元素
- boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
- 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
解题思路:
使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。
下面动画模拟以下队列的执行过程如下:
执行语句:
queue.push(1);
queue.push(2);
queue.pop(); 注意此时的输出栈的操作
queue.push(3);
queue.push(4);
queue.pop();
queue.pop();注意此时的输出栈的操作
queue.pop();
queue.empty();
在push数据的时候,只要数据放进输入栈就好,但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
Java代码实现:
class MyQueue {
Stack<Integer> stack1, stack2;
public MyQueue() {
stack1 = new Stack(); // 负责进栈
stack2 = new Stack(); // 负责出栈
}
public void push(int x) {
stack1.push(x);
}
public int pop() {
// 如果stack2为空,那么将stack1中的元素全部放到stack2中
if (stack2.isEmpty()){
while (!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
// 然后出栈
return stack2.pop();
}
public int peek() {
// 如果stack2为空,那么将stack1中的元素全部放到stack2中
if (stack2.isEmpty()){
while (!stack1.isEmpty()){
stack2.push(stack1.pop());
}
}
// 然后取stack2栈顶元素
return stack2.peek();
}
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
225. 用队列实现栈
leetcode题目链接:225. 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
- void push(int x) 将元素 x 压入栈顶。
- int pop() 移除并返回栈顶元素。
- int top() 返回栈顶元素。
- boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
- 你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
- 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
解题思路:
队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。
所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。
但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的!
如下面动画所示,用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
queue.push(1);
queue.push(2);
queue.pop(); // 注意弹出的操作
queue.push(3);
queue.push(4);
queue.pop(); // 注意弹出的操作
queue.pop();
queue.pop();
queue.empty();
两个队列实现栈的Java代码实现:
两个队列Queue实现,一般使用LinkedList。
class MyStack {
// // 两个队列Queue实现,一般使用LinkedList
// Queue<Integer> queue1, queue2;
Deque<Integer> que1;
public MyStack() {
queue1 = new LinkedList<>(); // 和栈中保持一样元素的队列
queue2 = new LinkedList<>(); // 辅助队列
}
public void push(int x) {
queue2.offer(x); // 先放在辅助队列中
while (!queue1.isEmpty()){
queue2.offer(queue1.poll());
}
Queue<Integer> queueTemp;
queueTemp = queue1;
queue1 = queue2;
queue2 = queueTemp; // 最后交换queue1和queue2,将元素都放到queue1中
}
public int pop() {
return queue1.poll();
}
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
一个队列实现栈的Java代码:
一个队列实现,双端队列Deque,Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst都可以使用。
class MyStack {
// 一个队列实现,双端队列Deque
// Deque 接口继承了 Queue 接口
// 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst
Deque<Integer> que1;
public MyStack() {
que1 = new ArrayDeque<>();
}
public void push(int x) {
que1.addLast(x); // 进来直接加到最后面
}
public int pop() {
int size = que1.size();
size--;
while (size-- > 0) {
que1.addLast(que1.peekFirst());
que1.pollFirst();
}
int res = que1.pollFirst();
return res;
}
public int top() {
return que1.peekLast();
}
public boolean empty() {
return que1.isEmpty();
}
}
2. 栈的经典问题
(1) 括号匹配问题
20. 有效的括号
leetcode题目链接:20. 有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
示例一:
输入:s = "()"
输出:true
示例二:
输入:s = "()[]{}"
输出:true
示例三:
输入:s = "([)]"
输出:false
解题思路:
先来分析一下 这里有三种不匹配的情况,
- 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。
- 第二种情况,括号没有多余,但是 括号的类型没有匹配上。
- 第三种情况,字符串里右方向的括号多余了,所以不匹配。
使用栈来存储左括号对应的右括号。然后遇到右括号和栈顶元素相同则出栈。
第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false
第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false
第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false
左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。
Java代码实现:
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
// 碰到左括号,就把相应得右括号入栈
if (ch == '(') {
stack.push(')');
}else if(ch == '[') {
stack.push(']');
}else if(ch == '{') {
stack.push('}');
}else if(stack.isEmpty() || stack.pop() != ch) { //栈空,说明右括号没找到对应得左括号,栈顶元素不是匹配的字符
return false;
}
}
// 遍历完栈不空,则说明左括号没有对应的右括号
return stack.isEmpty();
}
}
(2) 字符串去重问题
1047. 删除字符串中的所有相邻重复项
leetcode题目链接: 1047. 删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例一:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
解题思路:
要删除相邻相同元素,其实也是匹配问题,相同左元素相当于左括号,相同右元素就是相当于右括号,匹配上了就删除。
那么再来看一下本题:可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。
Java代码实现:
用栈来实现匹配问题,最后需要将栈里的元素转为字符串。
class Solution {
public String removeDuplicates(String s) {
// 用栈来存储元素
Stack<Character> stack = new Stack<>();
for(int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if(stack.isEmpty() || stack.peek() != ch) { // 栈空或栈顶元素等于ch
stack.push(ch);
}else {
stack.pop();
}
}
String res = "";
while (!stack.isEmpty()) { // 剩下的都是不重复元素
res = stack.pop() + res;
}
return res;
}
}
用字符串当栈。
class Solution {
public String removeDuplicates(String s) {
// 用字符串直接做栈
StringBuffer res = new StringBuffer();
// top为 res 的长度
int top = -1;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 当 top > 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
if (top >= 0 && res.charAt(top) == c) {
res.deleteCharAt(top);
top--;
// 否则,将该字符入栈,同时top++
} else {
res.append(c);
top++;
}
}
return res.toString();
}
}
不使用StringBuffer,StringBuilder太慢。
class Solution {
public String removeDuplicates(String s) {
// 不用StringBuffer
char[] str = s.toCharArray();
int top = -1;
for (int i = 0; i < s.length(); i++) {
if (top == -1 || str[top] != str[i]) {
str[++top] = str[i];
} else {
top--;
}
}
return String.valueOf(str, 0, top + 1);
}
}
(3)逆波兰表达式问题
150. 逆波兰表达式求值
leetcode题目链接:150. 逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。
有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例一:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例二:
输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
解题思路:
这和1047. 删除字符串中的所有相邻重复项
是差不错的,只不过本题不要相邻元素做消除了,而是做运算!
使用栈将数字存到栈里面,遇到运算符的时候进行两个数的计算。
Java实现代码:
(注意在leetcode中,由于jdk版本不同,不能使用==来判断字符串等于运算符,需要使用equals)
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(String s : tokens) {
if("+".equals(s) || "-".equals(s) || "*".equals(s) || "/".equals(s)) { // jdk版本问题,不能使用==
int num1 = stack.pop();
int num2 = stack.pop();
if("+".equals(s)) stack.push(num2 + num1);
if("-".equals(s)) stack.push(num2 - num1);
if("*".equals(s)) stack.push(num2 * num1);
if("/".equals(s)) stack.push(num2 / num1);
} else {
stack.push(Integer.valueOf(s));
}
}
int res = stack.peek();
return res;
}
}
3. 队列的经典问题
(1)滑动窗口最大值问题
239. 滑动窗口最大值
leetcode题目链接:239. 滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例一:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
解题思路:
这是使用单调队列的经典题目。
设计单调队列的时候,pop,和push操作要保持如下规则:
-
pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
-
push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
利用双端队列手动实现单调队列
用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (–> head) (右边为头结点,元素存的是下标)
Java实现代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
ArrayDeque<Integer> deque = new ArrayDeque<>();
int n = nums.length;
int[] res = new int[n - k + 1];
int idx = 0;
for(int i = 0; i < n; i++) {
// 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
// 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
while(!deque.isEmpty() && deque.peek() < i - k + 1){
deque.poll();
}
// 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offer(i);
// 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
if(i >= k - 1){
res[idx++] = nums[deque.peek()];
}
}
return res;
}
}
(2)求前 K 个高频元素问题
347. 前 K 个高频元素
leetcode题目链接:347. 前 K 个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例一:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例二:
输入: nums = [1], k = 1
输出: [1]
解题思路:
这道题目主要涉及到如下三块内容:
- 要统计元素出现频率
- 对频率排序
- 找出前K个高频元素
首先统计元素出现的频率,这一类的问题可以使用map来进行统计。
然后是对频率进行排序,这里我们可以使用一种 容器适配器就是优先级队列。
什么是优先级队列呢?
其实就是一个披着队列外衣的堆,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。
而且优先级队列内部元素是自动依照元素的权值排列。
们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。
寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描)
Java代码实现:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
int[] result = new int[k];
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
// 根据map的value值正序排,相当于一个小顶堆
PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o1.getValue() - o2.getValue());
for (Map.Entry<Integer, Integer> entry : entries) {
queue.offer(entry);
if (queue.size() > k) {
queue.poll();
}
}
for (int i = k - 1; i >= 0; i--) {
result[i] = queue.poll().getKey();
}
return result;
}
}
简单写法:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer,Integer> hashMap = new HashMap<>();
for(int m : nums)
{
hashMap.put(m,hashMap.getOrDefault(m,0)+1);
}
PriorityQueue<Integer> queue = new PriorityQueue<>((a,b) -> (hashMap.get(b)-hashMap.get(a)));
for(int key : hashMap.keySet())
{
queue.offer(key);
}
int res[] = new int[k];
for(int i=0;i<k;i++)
{
res[i] = queue.poll();
}
return res;
}
}
三、其它算法分析
1. 动态规划之背包问题——01背包
2. 动态规划之背包问题——完全背包
3. 动态规划之子序列问题
4. 算法分析之数组问题
5. 算法分析之链表问题
6. 算法分析之哈希表
7. 算法分析之字符串
8. 算法分析之栈和队列
参考: