算法通关村第5关| hash与队列|Leetcode 232. 用栈实现队列、225. 用队列实现栈、15. 三数之和

1. 用栈实现队列

232. 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 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. 用队列实现栈

  1. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 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. 两数之和

1. 两数之和

给定一个整数数组 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. 三数之和

15. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

**注意:**答案中不可以包含重复的三元组。

5.1 思路分析

这道题目可以像两数之和一样使用哈希法,但是有一个巨大的难点,就是元组不可以重复,所以说需要做去重操作,而去重的操作中有超级多的细节需要注意,在面试中很难写出bug free的代码。

虽然说哈希法也是时间复杂度为O(N^2)的算法,但是双指针法会比哈希法更加高效

  1. 拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。

  2. 处理边界的逻辑, 如果nums[i]已经大于0,那么不可能凑成三数和为0,直接结束这个for循环。

  3. 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} 这组数据一样可以收录到 结果集里。这是一个非常细节的思考过程。

  4. 依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i] b = nums[left] c = nums[right]

  5. 接下来如何移动left 和right呢,循环条件也应该是while(left<right),因为如果两者相等,那相当于只有两个数了,不符合题意;

  6. 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。

  7. 如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

  8. 这个过程中,会找到三数之和等于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 总结及反思

  1. 本题的双指针逻辑相对还比较清晰明了,但是去重的过程有很多容易出错的地方和细节;

  2. 既然三数之和可以使用双指针法,我们之前讲过的两数之和可不可以使用双指针法呢?

    答:两数之和 就不能使用双指针法,因为两数之和要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了,如果要求返回的是数值的话,就可以使用双指针法了。

度也为O(1);但如果题意不能原地排序的话,需要创建一个新的数组,那么排序的空间复杂度变为O(N)。

5.3 总结及反思

  1. 本题的双指针逻辑相对还比较清晰明了,但是去重的过程有很多容易出错的地方和细节;

  2. 既然三数之和可以使用双指针法,我们之前讲过的两数之和可不可以使用双指针法呢?

    答:两数之和 就不能使用双指针法,因为两数之和要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了,如果要求返回的是数值的话,就可以使用双指针法了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值