1. 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(
push
、pop
、peek
、empty
):实现
MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
思路:
- 用两个栈
- 一个作为输入栈,
push()
- 一个作为输出栈,
pop()
与peek()
- 一个作为输入栈,
- 每次pop活peek时
- 若输出栈为空——>将输入栈全部数据依次弹出输出栈——>输出栈中:栈顶到栈底的顺序即为队列从队首往队尾顺序(队列:先进先出)
class MyQueue {
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
stackIn = new Stack<>();//负责进栈
stackOut = new Stack<>();//负责出栈
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
dumpStackIn();
return stackOut.pop();
}
public int peek() {
dumpStackIn();
return stackOut.peek();
}
public boolean empty() {
//判断是否为空:若in不空,out为空:false&&true=false,即不空
return stackIn.isEmpty()&& stackOut.isEmpty();
}
private void dumpStackIn(){
//输出栈不为空
if (!stackOut.isEmpty()) {
return;
}
//输入栈不为空
while (!stackIn.isEmpty()) {
stackOut.push(stackIn.pop());
}
}
}
2. 用队列实现栈
- 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(
push
、top
、pop
和empty
)。实现
MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
思路:
- 首先想到,使用两个队列
- q 1存储栈内元素——>判断是否空,只需判断q1是否空即可
- q 2作为入栈的辅助队列
- 入栈
- 将元素入栈到q2,q1的全部元素依次出队并入队到q2
- 此时,q2前端元素即为新入栈元素
- q1与q2互换。——>q1元素即栈内元素,q1前后端分别对应栈顶与栈底。
class MyStack {
Queue<Integer> queue1;//存储栈内元素
Queue<Integer> queue2;//入栈的辅助队列
public MyStack() {
queue1=new LinkedList<>();
queue2=new LinkedList<>();
}
//图为整个入栈操作
public void push(int x) {
queue2.offer(x);
while (!queue1.isEmpty()) {//图中第2步,将q1元素出队入队到q2
queue2.offer(queue1.poll());
}
Queue<Integer> tmp=queue1;
queue1=queue2;
queue2=tmp;
}
//移出并返回
public int pop() {
return queue1.poll();
}
//返回栈顶
public int top() {
return queue1.peek();
}
public boolean empty() {
return queue1.isEmpty();
}
}
还可以用一个队列实现
3. 队列与栈方法
队列
方法 | 描述 |
---|---|
add() | 添加元素到队列尾部,若队列已满则抛出异常 |
offer() | 添加元素到队列尾部,若队列已满则返回false |
remove() | 移除并返回队列头部的元素,若队列为空则抛出异常 |
poll() | 移除并返回队列头部的元素,若队列为空则返回null |
element() | 返回队列头部的元素,但不移除,若队列为空则抛出异常 |
peek() | 返回队列头部的元素,但不移除,若队列为空则返回null |
size() | 返回队列中元素的个数 |
isEmpty() | 判断队列是否为空 |
clear() | 清空队列中的所有元素 |
栈
以下是栈(Stack)在Java中常用的方法,以表格形式列出:
方法 | 描述 |
---|---|
push() | 将元素推入栈顶 |
pop() | 移除并返回栈顶的元素(弹出栈顶元素) |
peek() | 返回栈顶的元素,但不从栈中移除它(获取栈顶元素但不弹出) |
size() | 返回栈中元素的个数 |
isEmpty() | 判断栈是否为空 |
clear() | 清空栈中的所有元素 |
4. 两数之和
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出 和为目标值target
的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
4.1思路
为什么想到用哈希法?
将遍历过的元素放到一个集合中,每次遍历新的位置的时候,就判断和目前遍历元素匹配的元素是否出现在这个集合中,如果出现就是遍历过
用什么样的哈希表?
在本题中,我们不仅要知道元素是否被遍历过,还要知道元素所对应的下标,所以使用map
- map用来存放遍历过的元素
- map的key用来存放元素, value用来存放下标;
- 因为要查的是元素是否出现过,所以应该把元素作为key,最后返回对应的value值,即下标。map的作用就是能在最快的时间内查找元素是否出现过
为什么不选用数组或者set呢?
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而这道题目,不仅要判断temp是否存在而且还要记录temp的下标位置,因为要返回对应的下标。所以set 也不能用
📍📍📍
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
4.2 解题代码
class Solution {
public int[] twoSum(int[] nums, int target) {
//map集合
HashMap<Integer, Integer> map = new HashMap<>();
//遍历数组
for (int i = 0; i < nums.length; i++) {
int temp = target - nums[i];
if (map.containsKey(temp)) {//在map中寻找是否有nums[i]匹配的key key存元素 value存下标
return new int[]{map.get(temp), i};//返回角标
}
//若不包含,则把数组添加入到map集合
map.put(nums[i], i);
}
return new int[0];
}
}
-
时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,可以O(1) 地寻找 target - x。
-
空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
4.3 总结及其注意
- map集合中key可以存的是数组元素,这样可以更快查找元素是否出现过。value存下标
- map集合中get()是取,put是存
- get()获取对应键的值
5. 三数之和
给你一个整数数组
nums
,判断是否存在三元组[nums[i], nums[j], nums[k]]
满足i != j
、i != k
且j != k
,同时还满足nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为
0
且不重复的三元组。**注意:**答案中不可以包含重复的三元组。
5.1 思路分析
这道题目可以像两数之和一样使用哈希法,但是有一个巨大的难点,就是元组不可以重复,所以说需要做去重操作,而去重的操作中有超级多的细节需要注意,在面试中很难写出bug free的代码。
虽然说哈希法也是时间复杂度为O(N^2)
的算法,但是双指针法会比哈希法更加高效。
-
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
-
处理边界的逻辑, 如果
nums[i]
已经大于0,那么不可能凑成三数和为0,直接结束这个for循环。 -
i元素的去重逻辑
应该是从第二个元素开始,如果和前面的元素一样的话,就直接略过当前的循环,进入下一个循环中。但这里很细节
-
如果写成
if(nums[i] == nums[i+1] )
当数组为【-1-1,0,0,2】这样子的时候,i = 0, nums[i] = -1; left = 1; right=4; 这种情况i比较后面数,就会自动i+ 1= 1开始考虑这个循环,会忽略掉-1 -1 2 这种情况。
所以说i=0 不比较,从i=1开始,和前面的比较,如果相同,则跳过,这才是正确的i去重的办法。我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
-
所以这里是有两个重复的维度。那么应该这么写:
if (i > 0 && nums[i] == nums[i - 1])
这么写就是当前使用nums[i]
,我们判断前一位是不是一样的元素。在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。这是一个非常细节的思考过程。
-
-
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于
a = nums[i] b = nums[left] c = nums[right]
。 -
接下来如何移动left 和right呢,循环条件也应该是
while(left<right)
,因为如果两者相等,那相当于只有两个数了,不符合题意; -
如果
nums[i] + nums[left] + nums[right] > 0
就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。 -
如果
nums[i] + nums[left] + nums[right] < 0
说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。 -
这个过程中,会找到三数之和等于0 的时刻,这时候,要先把它们加到result 里面去记录上,然后,在进行left和right的去重环节,这时候是拿下一个和现在的这个相比较,因为这个是已经加进去了,所以不会遗漏。
5.2 解题代码
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();//定义一个结果集
//对数组排序
Arrays.sort(nums);
//for循环遍历
for (int i = 0; i < nums.length; i++) {
//排序后若第一个元素已经大于0,无论怎么组合都不可能凑成三元组,直接返回结果
if (nums[i]>0){
return result;
}
//去重a
if (i>0&&nums[i]==nums[i-1])
continue;//直接进入下一个for循环
//双指针
int left=i+1,right=nums.length-1;
while (right>left){
//和
int sum=nums[i]+nums[left]+nums[right];
//对和进行讨论
//若和大于0,移动right向左使其减小
if (sum>0){
right--;
}else if (sum<0){
left++;
}else {
result.add(Arrays.asList(nums[i],nums[left],nums[right]));//Arrays.asList()是Arrays类中的一个静态方法,用于将数组转换为List集合。
//最后对b、c去重 去重在获取三元组之和 先获取最开始的,对之后的数要进行判断来达到去重目的
while (right>left&&nums[right]==nums[right-1]) right--;//right-1 不能right-- 下同
while (right>left&&nums[left]==nums[left+1]) left++;
//找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
}
- 时间复杂度:O(n^2)。
- 首先排序的时间复杂度为
O(NlogN)
;第一个循环,遍历元素i,在每个元素i中,两个指针left和right会把数组也遍历一遍,因此总共的遍历的复杂度是O(N2),加起来就是O(N2);
- 首先排序的时间复杂度为
- 空间复杂度:
O(1) / O(N)
- 输出答案的空间不会有很多,可以认为是O(1),排序所需要的空间复杂度也为O(1);但如果题意不能原地排序的话,需要创建一个新的数组,那么排序的空间复杂度变为O(N)。
5.3 总结及反思
-
本题的双指针逻辑相对还比较清晰明了,但是去重的过程有很多容易出错的地方和细节;
-
既然三数之和可以使用双指针法,我们之前讲过的两数之和可不可以使用双指针法呢?
答:两数之和 就不能使用双指针法,因为两数之和要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了,如果要求返回的是数值的话,就可以使用双指针法了。
度也为O(1);但如果题意不能原地排序的话,需要创建一个新的数组,那么排序的空间复杂度变为O(N)。
5.3 总结及反思
-
本题的双指针逻辑相对还比较清晰明了,但是去重的过程有很多容易出错的地方和细节;
-
既然三数之和可以使用双指针法,我们之前讲过的两数之和可不可以使用双指针法呢?
答:两数之和 就不能使用双指针法,因为两数之和要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了,如果要求返回的是数值的话,就可以使用双指针法了。