【Java】HOT100 二分查找、栈与堆

目录

一、二分查找(通常用来做有序查找)

LeetCode35:搜索插入位置

LeetCode74:搜索二维矩阵

LeetCode34:在排序数组中查找元素的第一个和最后一个位置

LeetCode33:搜索旋转排序数组

LeetCode153:寻找旋转排序数组中的最小值

LeetCode4:寻找两个正序数组的中位数

二、栈(匹配对称、单调栈)

LeetCode20:有效的括号(匹配对称)

LeetCode155:最小栈

LeetCode739:每日温度

LeetCode496:下一个最大元素i

LeetCode503:下一个最大元素ii

LeetCode42:接雨水

LeetCode84:柱状图中的最大矩形

LeetCode394:字符串解码

三、堆

LeetCode215:数组中的第K个最大元素

LeetCode347:前K个高频元素

LeetCode295:数据流的中位数


一、二分查找(通常用来做有序查找)

LeetCode35:搜索插入位置

思路:类似于704.二分查找,这里利用的是左闭右开的区间,return的不是-1是left

关键词:有序查找

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length;
        while (left < right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target)
                return mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid;
        }
        return left;
    }
}

LeetCode74:搜索二维矩阵

思路: 每行都二维查找

class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
		for (int[] row : matrix) {
			int index = search(row, target);
			if (index >= 0) {
				System.out.println("true");
				return true;
			}
		}
		return false;
	}

	// 把当行进行二分查找
	public static int search(int[] nums, int target) {
		int low = 0, high = nums.length - 1;
		while (low <= high) {
			int mid = (high - low) / 2 + low;
			int num = nums[mid];
			if (num == target) {
				return mid;
			} else if (num > target) {
				high = mid - 1;
			} else {
				low = mid + 1;
			}
		}
		return -1;
	}
}

LeetCode34:在排序数组中查找元素的第一个和最后一个位置

思路:先二分查找到,再滑动窗口(如果数太多了滑动太慢,就二分找左右界)

 ① 滑动窗口法

class Solution {
    int[] res = new int[2];
    public int[] searchRange(int[] nums, int target) {
        int left = 0, right = nums.length;
        while (left < right) {
            int mid = left + ((right - left) >> 1);
            if (nums[mid] == target){   //找到目标值
                int l = mid, r = mid;
                //滑动缩小搜索区间
                while(l-1>=left && nums[l-1] == target){
                    l--;
                }
                while(r+1<right && nums[r+1] == target){
                    r++;
                }
                return new int[]{l,r};
            }   
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid;
        }
        return new int[]{-1,-1};
    }
}

②二分法查找左右边界,这样计算出来的右边界是不包括target的右边界,左边界同理。

class Solution {
    int[] searchRange(int[] nums, int target) {
        int leftBorder = getLeftBorder(nums, target);
        int rightBorder = getRightBorder(nums, target);
        // 情况一:超出左右边界
        if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1};
        // 情况三:存在
        if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1};
        // 情况二:不存在target
        return new int[]{-1, -1};
    }

    int getRightBorder(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1;
            } else { // 寻找右边界,nums[middle] == target的时候更新left
                left = middle + 1;
                rightBorder = left;
            }
        }
        return rightBorder;
    }

    int getLeftBorder(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                leftBorder = right;
            } else {
                left = middle + 1;
            }
        }
        return leftBorder;
    }
}

LeetCode33:搜索旋转排序数组

思路:如果中间的数小于最右边的数,则右半段是有序的,若中间数大于最右边数,则左半段是有序的,这样就可以保留有序的半段,来搜索target了。

首先二分的根本是有序,只要有序就能二分,哪怕是部分有序(这个是重点!!)

class Solution {
    public int search(int[] nums, int target) {
        int len = nums.length;
        int left = 0, right = len-1;
        while(left <= right){
            int mid = (left + right) / 2;
            if(nums[mid] == target)
                return mid;
            else if(nums[mid] < nums[right]){   //右半段有序
                if(nums[mid] < target && target <= nums[right])
                    left = mid+1;
                else
                    right = mid-1;
            }
            else{   //左半段有序
                if(nums[left] <= target && target < nums[mid])
                    right = mid-1;
                else
                    left = mid+1;
            }
        }
        return -1;
    }
}

LeetCode153:寻找旋转排序数组中的最小值

旋转升序数组的性质:如果中间元素大于等于左端元素,说明最小值位于右半部分;如果中间元素小于等于右端元素,说明最小值位于左半部分。

class Solution {
    public int findMin(int[] nums) {
        int len = nums.length;
        int left = 0, right = len-1;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] > nums[right]){   //右半段无序
                left = mid+1;   //到右半段查找最小值
            }
            else{   //左半段无序
                right = mid;
            }
        }
        return nums[left];
    }
}

LeetCode4:寻找两个正序数组的中位数

思路:归并排序

  • i < nums1.length && (j >= nums2.length || nums1[i] < nums2[j])——
  • 如果nums1还未处理完且nums2处理完了,或者nums1和nums都未处理完,取较小值
  • index <= totalLength /2(无论奇数偶数都只要循环到这个值)
  • totalLength & 1 == 0(偶数),totalLength & 1 == 1(奇数)

归并排序:没并完。

其实和②是一个道理,只不过用了nums数组储存,②是用pre和now变量来记录

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {

        int totalLength = nums1.length + nums2.length;
        int[] nums = new int[totalLength];
        int i = 0, j = 0;
        int index = 0;
        while (index <= totalLength /2) {
            if (i < nums1.length && (j >= nums2.length || nums1[i] < nums2[j])) {
                nums[index] = nums1[i++];
            } else{
                nums[index] = nums2[j++];
            }
            if (index == totalLength / 2) {
                //nums数组长度为偶数
                if ((totalLength & 1) == 0) {
                    return (nums[index] + nums[index - 1]) / 2.0;
                } else {
                    //nums数组长度是奇数
                    return 1.0 * nums[index];
                }
            }
            index++;
        }
        return 0.0;
    }
}

②找到中位数,不合并

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;
        int len = m + n;
        //当前指向nums1数组和nums2数组的位置。
        int aStart = 0, bStart = 0;
        int pre = -1, now = -1;
        //奇数偶数都需要遍历len / 2个数
        for (int i = 0; i <= len / 2; i++) {
            pre = now;
            //如果nums1还未处理完且nums2处理完了,或者nums1和nums都未处理完,取较小值赋给now
            if (aStart < m && (bStart >= n || nums1[aStart] < nums2[bStart]) ) {
                now = nums1[aStart++];
            } else {
                now = nums2[bStart++];
            }
        }
        if ((len & 1) != 1) {   //偶数个
            return (pre + now) / 2.0;
        }
        return now * 1.0;   //奇数个
    }
}

二、栈(匹配对称、单调栈)

栈和队列之前做过一个总结在这:【Java】栈和队列:总结-CSDN博客

栈非常适合做匹配对称(即有顺序的匹配)题目。

LeetCode20:有效的括号(匹配对称)

思路:push进匹配括号,再判断是否重复,不重复就返回false,重复就pop;

class Solution {
    Deque<Character> q1 = new LinkedList<>();
    public boolean isValid(String s) {
        int size = s.length();
        char ch;
        for(int i=0;i<size;i++){
            ch = s.charAt(i);
            if(ch=='(')  q1.push(')');
            else if(ch=='{')  q1.push('}');
            else if(ch=='[')  q1.push(']');
            else if(q1.isEmpty() || q1.peek()!=ch){
                return false;
            }else{
                q1.pop();
            }
        }
        return true;
    }
}

LeetCode155:最小栈

思路:维护两个栈,一个数据栈,一个最小栈

class MinStack {
    Stack<Integer> data_stack;
    Stack<Integer> min_stack;
    public MinStack() {
        data_stack = new Stack<>();
        min_stack = new Stack<>();
    }
    
    public void push(int val) {
        data_stack.push(val);
        if(min_stack.isEmpty()){
            min_stack.push(val);
        }else{
            if(val > min_stack.peek()){
                val = min_stack.peek();
            }
            min_stack.push(val);
        }
    }
    
    public void pop() {
        data_stack.pop();
        min_stack.pop();
    }
    
    public int top() {
        return data_stack.peek();
    }
    
    public int getMin() {
        return min_stack.peek();
    }
}

LeetCode739:每日温度

思路:单调栈的典型扫盲题。通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。其本质是空间换时间。

单调栈存放的是元素的下标,顺序是栈头到栈底递增,也就是说栈底要是最大的

如果遍历的元素大于栈顶,则弹出栈顶,加入元素

如果遍历的元素小于或者等于栈顶,则无需弹出直接加入元素,该单调栈仍然保持递增【75、71...

class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int lens=temperatures.length;
        int []res=new int[lens];
        Deque<Integer> stack=new LinkedList<>();    //res记录最大值出现在右边第几个(差值)
        for(int i=0;i<lens;i++){

           while(!stack.isEmpty()&&temperatures[i]>temperatures[stack.peek()]){
                    res[stack.peek()]=i-stack.peek();   //此时遍历的i就是当前天数的,下一个更高温度的下标,
                    stack.pop();
                }
                stack.push(i);
        }

        return  res;
    }
}

LeetCode496:下一个最大元素i

思路:明确被弹出元素的更大值就是当前序号为i的元素,不同的是res数组可以选择存储天数差值,也可以选择存储更大值。

class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        int []res=new int[nums1.length];
        Arrays.fill(res,-1);
        Deque<Integer> stack=new LinkedList<>();
        Map<Integer,Integer> map = new HashMap<>();
        for(int i=0;i<nums1.length;i++){
            map.put(nums1[i],i);
        }
        for(int i=0;i<nums2.length;i++){
            while(!stack.isEmpty()&&nums2[i]>nums2[stack.peek()]){
                //明确被弹出元素的更大值就是当前序号为i的元素
                if(map.containsKey(nums2[stack.peek()])){
                    int index = map.get(nums2[stack.peek()]);
                    res[index] = nums2[i];
                }
                stack.pop();
            }
            stack.push(i);
        }
        return  res;
    }
}

LeetCode503:下一个最大元素ii

思路:可以模拟将两个数组拼到一起,这样就可以求出循环数组的较大值。

还有千万别忘了res用-1填满

class Solution {
    public int[] nextGreaterElements(int[] nums) {
        int len = nums.length;
        int[] res = new int[len];
        Arrays.fill(res,-1);
        if(len <1 || nums == null){
            return new int[]{-1};
        }
        Deque<Integer> stack = new LinkedList<>();
        for(int i = 0;i < 2 * len;i++){
            while(!stack.isEmpty() && nums[i%len] > nums[stack.peek()]){
                res[stack.peek()] = nums[i%len];
                stack.pop();
            }
            stack.push(i%len);
        }
        return res;
    }
}

LeetCode42:接雨水

思路:这道题之前写过,有两种解法,即双指针和单调栈。

总体思路是,遍历每一个列,需要左右边最高列其中的较矮列>当前列才能储存水,且存水量等于二者之差。

我们正需要寻找一个元素,右边最大元素以及左边最大元素,这正是单调栈所做的事情

 739. 每日温度中求一个元素右边第一个更大元素,单调栈就是递增的,84.柱状图中最大的矩形 求一个元素右边第一个更小元素,单调栈就是递减的。

双指针解法(按列):

class Solution {
    public int trap(int[] height) {
        int length = height.length;
        if (length <= 2) return 0;
        int[] maxLeft = new int[length];
        int[] maxRight = new int[length];

        // 记录每个柱子左边柱子最大高度
        maxLeft[0] = height[0];
        for (int i = 1; i< length; i++) maxLeft[i] = Math.max(height[i], maxLeft[i-1]);

        // 记录每个柱子右边柱子最大高度
        maxRight[length - 1] = height[length - 1];
        for(int i = length - 2; i >= 0; i--) maxRight[i] = Math.max(height[i], maxRight[i+1]);

        // 求和
        int sum = 0;
        for (int i = 0; i < length; i++) {
            int count = Math.min(maxLeft[i], maxRight[i]) - height[i];
            if (count > 0) sum += count;
        }
        return sum;
    }
}
class Solution {
    public int trap(int[] height) {
        int left = 0,right = height.length-1;
        int ret = 0;
        int left_max = 0,right_max = 0;
        while (left < right){
            if (height[left] < height[right]){
                if (height[left]>left_max){
                    left_max = height[left];
                }else{
                    ret += left_max - height[left];
                }
                left++;
            }else{
                if(height[right] >= right_max){
                    right_max = height[right];
                }else{
                    ret += right_max - height[right];
                }
                right--;
            }
        }
        return ret;
    }
}

单调栈是按行来计算的,【2,1,        3 :分别代表左柱子,当前柱子和右柱子

求较大值,递增;按行来计算不同按列计算的一点在于对于相同的柱子

class Solution {
    public int trap(int[] height){
        int size = height.length;
        if (size <= 2) return 0;
        Stack<Integer> stack = new Stack<Integer>();
        stack.push(0);
        int sum = 0;
        for (int index = 1; index < size; index++){
            int stackTop = stack.peek();
            if (height[index] < height[stackTop]){
                stack.push(index);
            }else if (height[index] == height[stackTop]){
                // 因为相等的相邻墙,左边一个是不可能存放雨水的,所以pop左边的index, push当前的index
                stack.pop();
                stack.push(index);
            }else{
                //pop up all lower value
                int heightAtIdx = height[index];
                while (!stack.isEmpty() && (heightAtIdx > height[stack.peek()])){
                    int mid = stack.pop();
                    if (!stack.isEmpty()){
                        int left = stack.peek();
                        int h = Math.min(height[left], height[index]) - height[mid];
                        int w = index - left - 1;
                        int hold = h * w;
                        if (hold > 0) sum += hold;
                    }
                }
                stack.push(index);
            }
        }
        return sum;
    }
}

LeetCode84:柱状图中的最大矩形

思路:

数组前方添加一个0,这是为了保证有三个数(left、right、mid)可比较

数组后方加一个0,这是为了防止数组本身就是递减,所有数加入栈后还未处理max

由于是要找出mid左右的较小值,所以栈是递减栈

class Solution {
    public int largestRectangleArea(int[] heights) {
        Stack<Integer> stack = new Stack<Integer>();

        // 数组扩容,在头和尾各加入一个元素
        int[] newHeight = new int[heights.length + 2];
        System.arraycopy(heights, 0, newHeight, 1, heights.length);
        newHeight[heights.length+1] = 0;
        newHeight[0] = 0;

        stack.push(0);
        int max = 0;
        for (int i = 1; i < newHeight.length; i++) {
            while (newHeight[i] < newHeight[stack.peek()]) {
                int mid = stack.pop();
                int w = i - stack.peek() - 1;
                int h = newHeight[mid];
                max = Math.max(max, w * h);
            }
            stack.push(i);

        }
        return max;
    }
}


LeetCode394:字符串解码

思路:这道题类似于逆波兰表达式求值,也是匹配类题目,但是他用到两个栈,即数字栈和字符串栈,可以更好地管理内存,同时用StringBuilder类型减少了字符串拼接的操作。

class Solution {
    public String decodeString(String s) {
        StringBuilder sb = new StringBuilder();
        int num = 0;
        Stack<Integer> numStack = new Stack<>();
        Stack<StringBuilder> strStack = new Stack<>();
        
        for (char c : s.toCharArray()) {
            if (Character.isDigit(c)) {
                num = num * 10 + (c - '0');
            } else if (c == '[') {
                numStack.push(num);
                strStack.push(sb);
                //进入到新 [ 后,sb 和 num 重新记录,置空
                num = 0;
                sb = new StringBuilder();
            } else if (c == ']') {
                StringBuilder temp = sb;    //记录当前字符串
                sb = strStack.pop();    //衔接上原来的字符串
                int repeatTimes = numStack.pop();
                for (int i = 0; i < repeatTimes; i++) {
                    sb.append(temp);
                }
            } else {
                sb.append(c);
            }
        }
        
        return sb.toString();
    }
}

三、堆

LeetCode215:数组中的第K个最大元素

思路:使用java提供的API PriorityQueue,构建小顶堆

class Solution {
    public int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> queue = new PriorityQueue<>(); //小顶堆
        for(int num : nums){
            queue.add(num);
            if(queue.size() > k){
                queue.poll();
            }
        }
        return queue.peek();
    }
}

LeetCode347:前K个高频元素

思路:首先统计出现频率,就要立马想到用map;其次,对频率进行排序,要用到优先级队列.

class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        //第一步:通过map来计数
        Map<Integer,Integer> map = new HashMap<>();
        for(int num:nums){
            map.put(num,map.getOrDefault(num,0)+1);
        }
        //第二步:构建小顶堆,比较器设置为value的升序
        PriorityQueue<int[]> pq = new PriorityQueue<>((a,b)->a[1]-b[1]);
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){
            //先用k个元素初始化,再开始比较
            if(pq.size()<k){
                pq.add(new int[]{entry.getKey(),entry.getValue()});
            }
            else{
                //弹出较小元素,留下的就是最大元素
                if(entry.getValue()>pq.peek()[1]){
                    pq.poll();
                    pq.add(new int[]{entry.getKey(),entry.getValue()});
                }
            }
        }
        //第三步:创建res数组存放前k个元素,即剩下的k个元素
        int[] res = new int[k];
        for(int i=0;i<k;i++){
            res[i] = pq.poll()[0];
        }
        return res;
    }
}

LeetCode295:数据流的中位数

思路:

在数据流中,数据会不断涌入结构中,那么也就面临着需要多次动态调整以获得中位数。 因此实现的数据结构需要既需要快速找到中位数,也需要做到快速调整。首先能想到就是二叉搜索树,在平衡状态下,树顶必定是中间数,然后再根据长度的奇偶性决定是否取两个数。

此方法效率高,但是手动编写较费时费力。

根据只需获得中间数的想法,可以将数据分为左右两边,一边以最大堆的形式实现,可以快速获得左侧最大数, 另一边则以最小堆的形式实现。其中需要注意的一点就是左右侧数据的长度差不能超过1。 当两个顶堆个数相等时,大顶堆存放较小的数,如果新加的数大于大顶堆的top,则加入小顶堆,加入新数后又不均衡了,把小顶堆的top加入大顶堆。来回切换

edianFinder {
    // 大顶堆与小顶堆,小顶堆默认创建就是小顶堆
    PriorityQueue<Integer> maxHeap;
    PriorityQueue<Integer> minHeap;

    public MedianFinder() {
        maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
        minHeap = new PriorityQueue<>();
    }

    // 每次添加元素时,如果两个堆的大小相等,就往大顶堆加,否则往小顶堆加
    //总数奇数,大顶堆比小顶堆大1;总数偶数,大顶堆小顶堆一样大
    public void addNum(int num) {
        if (maxHeap.size() == minHeap.size()) {
            // 大顶堆=小顶堆的时候,就像大顶堆加,加之前加入小顶堆排序,然后加入小顶堆的最小值
            minHeap.offer(num);
            maxHeap.offer(minHeap.poll());
        } else {
           // 大顶堆>小顶堆的时候,就向小顶堆加,加之前加入大顶堆排序,然后加入大顶堆的最大值
            maxHeap.offer(num);
            minHeap.offer(maxHeap.poll());
        }
    }

    public double findMedian() {
        // 相等的话直接取中位数 否则取大顶堆,因为大顶堆的大小>=小顶堆
        if (maxHeap.size() == minHeap.size()) {
            return (maxHeap.peek() + minHeap.peek()) / 2.0;
        } else {
            return maxHeap.peek();
        }
    }
}

  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值