算法通关村第五关——队栈和Hash的经典算法(白银)

算法通关村第五关——队栈和Hash的经典算法(白银)

    • 1. 用栈实现队列
    • 2. 用队列实现栈
    • 3. n数之和专题
      • 3.1 两数之和
        • 方法一:双for的遍历
        • 方法二:使用Hash表
      • 3.2 三数之和
        • 方法一:排序+双指针
        • 方法二:优化双指针

1. 用栈实现队列

leetcode 232. 用栈实现队列

这个算法的实现思路是使用两个栈来模拟队列的入队和出队操作。其中,inStack 栈用于入队操作,outStack 栈用于出队操作。

  1. 入队操作(push)时,直接将元素压入 inStack 栈即可。

  2. 出队操作(pop)时,如果 outStack 栈为空,则需要先将 inStack 栈中的元素全部转移至 outStack 栈,然后再从 outStack 栈顶弹出一个元素作为出队结果。这样做的原因是保证出队的元素按照队列的顺序进行。

  3. 获取队头元素操作(peek)与出队操作类似,也需要先判断 outStack 栈是否为空,若为空则将 inStack 栈中的元素转移到 outStack 栈,然后取出 outStack 栈顶元素作为队头元素。

为什么要这么做呢?

在使用单个栈实现队列时,入队操作只需要将元素压入栈即可,但出队操作需要将栈底的元素作为出队元素,这涉及到将栈中的元素逐个弹出并重新压入栈中的过程,时间复杂度为 O(n)。而使用两个栈来模拟队列,可以将元素从一个栈中转移到另一个栈中,从而实现出队操作的效果。由于转移元素只需一次,所以平摊下来,出队操作的时间复杂度为 O(1),与入队操作保持一致。

通过这种方式,我们可以实现以较低的时间复杂度执行队列的入队、出队和获取队头元素操作,提高了算法的效率。

class MyQueue {
    Deque<Integer> inStack;  // 入队操作使用的栈
    Deque<Integer> outStack;  // 出队和获取队头元素操作使用的栈

    public MyQueue() {
        inStack = new LinkedList<Integer>();  // 初始化入队栈
        outStack = new LinkedList<Integer>();  // 初始化出队和获取队头元素栈
    }

    public void push(int x) {
        inStack.push(x);  // 入队操作,直接将元素压入入队栈
    }

    public int pop() {
        if (outStack.isEmpty()) {
            in2out();  // 如果出队栈为空,则将入队栈中的元素转移到出队栈
        }
        return outStack.pop();  // 弹出出队栈顶元素作为出队结果
    }

    public int peek() {
        if (outStack.isEmpty()) {
            in2out();  // 如果出队栈为空,则将入队栈中的元素转移到出队栈
        }
        return outStack.peek();  // 返回出队栈顶元素作为队头元素
    }

    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();  // 判断队列是否为空
    }

    private void in2out() {
        while (!inStack.isEmpty()) {
            outStack.push(inStack.pop());  // 将入队栈中的元素逐个转移到出队栈
        }
    }
}

2. 用队列实现栈

leetcode 225. 用队列实现栈

这个实现的核心思想是将新元素插入到空的 queue2 中,并将 queue1 中的所有元素依次插入到 queue2 的后面。最后,交换 queue1 和 queue2 的引用,使得 queue1 成为包含所有元素的队列,而 queue2 变为空队列。

class MyStack {
    Queue<Integer> queue1;
    Queue<Integer> queue2;

    public MyStack() {
        queue1 = new LinkedList<Integer>();
        queue2 = new LinkedList<Integer>();
    }
    
    public void push(int x) {
        queue2.offer(x);
        while(!queue1.isEmpty()){
            queue2.offer(queue1.poll());
        }
        Queue<Integer> temp = queue1;
        queue1 = queue2;
        queue2 = temp;
    }
    
    public int pop() {
        return queue1.poll();
    }
    
    public int top() {
        return queue1.peek();
    }
    
    public boolean empty() {
        return queue1.isEmpty();
    }
}

3. n数之和专题

3.1 两数之和

leetcode 1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

方法一:双for的遍历

这种方式没什么好说的

class Solution {
    public int[] twoSum(int[] nums, int target) {
        for(int n = 0; n < nums.length; n++){
            for(int m = n+1; m < nums.length; m++){
                if(nums[n]+nums[m] == target){
                    return new int[]{n,m};
                }
            }
        }
        return new int[0];
    }
}

方法二:使用Hash表

算法的实现思路如下:

  1. 创建一个哈希表 hashTable 用于存储已经遍历过的元素及其对应的索引。
  2. 遍历数组nums中的每个元素,假设当前元素为nums[i]
    • 检查哈希表中是否存在键为 target - nums[i] 的元素,如果存在,则找到了两个数的和为 target,返回它们的索引。
    • 如果不存在,将当前元素 nums[i] 及其索引 i 存入哈希表中。
  3. 如果遍历完整个数组后仍然没有找到满足条件的两个数,则返回一个空数组表示无解。
class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> hashTable = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            if (hashTable.containsKey(target - nums[i])) {
                return new int[]{hashTable.get(target - nums[i]), i};
            }
            hashTable.put(nums[i], i);
        }
        return new int[0];
    }
}

3.2 三数之和

leetcode 15. 三数之和

这里使用了双指针的方法

方法一:排序+双指针

算法的实现思路如下:

  1. 首先对数组 nums 进行排序。
  2. 遍历数组中的每个元素nums[i],将其作为三数之和中的第一个数。
    • 如果 nums[i] 大于 0,那么三数之和一定大于 0,不可能存在满足条件的解,直接返回结果。
    • 如果 nums[i] 与前一个元素相同,为了避免重复计算,跳过当前循环。
  3. 使用双指针法,在剩余的数组中寻找两个数的和等于target - nums[i],其中左指针j从i+1开始,右指针k从数组末尾开始。
    • 如果 nums[j] + nums[k] > target - nums[i],将右指针向左移动,减小和的大小。
    • 如果 nums[j] + nums[k] < target - nums[i],将左指针向右移动,增大和的大小。
    • 如果nums[j] + nums[k] == target - nums[i],找到一组满足条件的解,保存并同时移动左右指针。
      • 避免重复解的出现,需要跳过与已经使用过的元素相同的元素。
  4. 继续遍历数组中的下一个元素,重复以上步骤。
  5. 返回保存的所有满足条件的解。

通过使用双指针法和排序算法,可以以较低的时间复杂度 O(n^2) 解决这个问题,其中 n 是数组的长度。这是因为排序算法的时间复杂度为 O(nlogn),而寻找满足条件的解的过程为双指针法,时间复杂度为 O(n)。

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        ArrayList<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);  // 对数组进行排序,方便使用双指针法
        int len = nums.length;
        if (len < 3 || nums[0] > 0 || nums[len - 1] < 0 || nums == null)
            return res;  // 如果数组长度小于3或者数组中最小值大于0、最大值小于0,则直接返回空结果
        
        for (int i = 0; i < len - 2; i++) {
            if (i > 0 && nums[i] == nums[i - 1])
                continue;  // 避免重复计算,如果当前元素与前一个元素相同,则跳过当前循环
            
            int j = i + 1;
            int k = len - 1;
            
            while (j < k) {
                int sum = nums[i] + nums[j] + nums[k];  // 计算三个数的和
                
                if (sum > 0) {
                    k--;  // 和大于0时,将右指针向左移动,减小和的大小
                } else if (sum < 0) {
                    j++;  // 和小于0时,将左指针向右移动,增大和的大小
                } else {
                    res.add(Arrays.asList(nums[i], nums[j], nums[k]));  // 找到一组满足条件的解,保存到结果中
                    
                    while (j < k && nums[j] == nums[j + 1])
                        j++;  // 跳过与已经使用过的元素相同的元素,避免重复解的出现
                    while (j < k && nums[k] == nums[k - 1])
                        k--;  // 跳过与已经使用过的元素相同的元素,避免重复解的出现
                    
                    j++;
                    k--;
                }
            }
        }
        
        return res;  // 返回所有满足条件的解
    }
}

方法二:优化双指针

其实这个方法跟上一个差不多,但是这个更好理解

算法的实现思路如下:

  1. 首先对数组 nums 进行排序。
  2. 枚举第一个数a,从数组的起始位置开始遍历。
    • 如果当前数字与前一个数字相同,则跳过当前循环,避免重复计算。
  3. 初始化第三个数 c 指针为数组最右端,初始化目标值 target-nums[first]
  4. 枚举第二个数b,从第一个数之后的位置开始遍历。
    • 如果当前数字与前一个数字相同,则跳过当前循环,避免重复计算。
    • 通过双指针法,在剩余的数组中寻找两个数的和等于 target,其中左指针指向 second+1,右指针指向 n-1
    • 判断左指针和右指针指向的数字之和:
      • 如果大于 target,则将右指针向左移动,减小和的大小。
      • 如果小于 target,则将左指针向右移动,增大和的大小。
      • 如果等于target,找到一组满足条件的解,保存到结果中,并同时移动左右指针。
        • 避免重复解的出现,需要跳过与已经使用过的元素相同的元素。
  5. 继续枚举下一个第一个数 a,重复以上步骤。
  6. 返回保存的所有满足条件的解。
int n = nums.length;
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<>();
// 枚举 a
for (int first = 0; first < n; ++first) {
    // 需要和上一次枚举的数不相同
    if (first > 0 && nums[first] == nums[first - 1]) {
        continue;
    }
    // c 对应的指针初始指向数组的最右端
    int third = n - 1;
    int target = -nums[first];
    // 枚举 b
    for (int second = first + 1; second < n; ++second) {
        // 需要和上一次枚举的数不相同
        if (second > first + 1 && nums[second] == nums[second - 1]) {
            continue;
        }
        // 需要保证 b 的指针在 c 的指针的左侧
        while (second < third && nums[second] + nums[third] > target) {
            --third;
        }
        // 如果指针重合,随着 b 后续的增加
        // 就不会有满足 a+b+c=0 并且 b
        if (second == third) {
            break;
        }
        if (nums[second] + nums[third] == target) {
            List<Integer> list = new ArrayList<>();
            list.add(nums[first]);
            list.add(nums[second]);
            list.add(nums[third]);
            ans.add(list);
        }
    }
}
return ans;

通过使用双指针法和排序算法,可以以较低的时间复杂度 O(n^2) 解决这个问题,其中 n 是数组的长度。这是因为排序算法的时间复杂度为 O(nlogn),而寻找满足条件的解的过程为双指针法,时间复杂度为 O(n)。

你可能感兴趣的:(数据结构,算法,算法,哈希算法,数据结构,java,笔记)