LeetCode -贪心/双指针/二分查找/排序

一. 贪心算法

1. 区间问题

例题452. 用最少数量的箭引爆气球。

  • 按照数组元素后一位进行排序,第一个区间的end大于等于后面区间的start时,这几个区间可用一支箭;
  • 区间问题,首先想想应该按照哪个元素进行排序。经常需要对第二个元素进行排序。
  • 从小到大排序写法:Arrays.sort(points, (a, b) -> a[1] < b[1] ? -1 : 1);
    Arrays.sort(points, (a, b) -> a[1] - b[1] ); 可能会出现溢出

二. 双指针

1. 典型 Two Sum

例题167。采用相反的两个指针遍历数组。一个初始指向最小的元素,即数组最左边,向右遍历;一个初始指向最大的元素,即数组最右边,向左遍历。

2. 归并数组

例题88。归并两个有序数组使之成为一个有序数组。如果不重新开辟空间,直接将数组b中的元素插入到数组a中,会出现数组a后续的元素被覆盖的情况(元素需要不断后移)。
所以,如果数组的数据有被覆盖的风险,可以记得用倒序!!!

3. 快慢指针

例题142。判断一个链表有没有环,并且找出入口地址。

  • 快慢指针先判断有没有环;
  • 快慢指针相遇后,慢指针继续走,另外再让一个指针从头开始走,两者相遇的地方就是环的入口;可以画图计算证明。

4. 滑动窗口

例题 76 给你一个字符串 s 、一个字符串 t ,返回 s 中涵盖 t 所有字符的最小子串。

  • 用一个滑动窗口在字符串 s 中滑行,判断是否包含t中所有字符;
  • 先移动右窗口 j,将 t 中所有字符包含进来,再移动 i,使窗口恰好包含所有 t 中字符;
  • 记录下满足条件的子串后,继续移动一格 i,此时窗口内则不包含所有字符,重新移动 j 直至再次满足条件,再移动 i 缩小至恰好满足条件。循环直至找到最小的子串。
  • 如何判断窗口内是否包含所有 t 中字符?维护一个 need 数组,存储包含 t 还需每个字符的数量,再维护一个元素 needNum,存储包含 t 总的尚需元素数量。当 needNum 中对应的元素数量减少为 0 时,表示该元素已全部包含;当 needNum = 0 时,表示窗口内包含所有字符。

PS:对字母进行数量存储无需使用 hashmap,可以使用 int 数组,用 asci 值代表数组下标,存储对应字母的个数。

//如下式可存储 A至z的 字母个数
int[] need = new int[128];
need[c - 'A'] += 1; // c代表某一字符
class Solution {
    //滑动窗口
    //另外一个难点在于如何判断窗口中是否包含所有字符
    public String minWindow(String s, String t) {
        if (t == "") {
            return "";
        }
        //维护一个数组用于存储尚需的每个字符的数量
        int[] need = new int[128];
        char[] tarr = t.toCharArray();
        for (int k = 0; k < tarr.length; k++) {
            need[tarr[k] - 'A'] ++;
        }
        //用于存储尚需字符的总数量
        int needNum = t.length();
        int i = 0;
        int j = 0;
        char[] sarr = s.toCharArray();
        String res = "";
        while (i <= j && j < sarr.length) {
            //找到了包含的字符
            if (need[sarr[j] - 'A'] > 0) {
                needNum --;
            }
            need[sarr[j] - 'A'] --;
            j++;
            //窗口内包含了所有t中的字符,移动左边界
            if (needNum == 0) {
                while (i <= j) {
                    //说明左边已经缩至最小
                    if (need[sarr[i] - 'A'] == 0) {
                        if ("".equals(res) || res.length() > j - i) {
                            res = s.substring(i, j);
                        }
                        //存储结果以后,继续移动左边界使之不满足条件
                        need[sarr[i] - 'A']++;
                        i ++;
                        needNum++;
                        break;
                    }
                    need[sarr[i] - 'A']++;
                    i ++;
                }
            }
        }
        return res;

    }
}

三. 二分查找

  • 二分查找的循环判断条件通常是 left <= right,此时内部处理是 left = mid + 1, right = mid - 1;
  • 也可以是 left < mid,此时内部处理是 left = mid + 1, right = mid;
  • 但都不一定完全对,需要具体题目具体分析,代入边界情况考虑。

1. 求开方

例题69。求一个数的开方,忽略小数部分。

  • 二分查找,最后返回的是右区间;
  • 避免溢出可以将乘法改成除法。
class Solution {
    public int mySqrt(int x) {
        if(x == 0) {
            return 0;
        }
        int i = 1;
        int j = x;
        int mid = 0;
        while (i <= j) {
            mid = (j + i) / 2;
            if (mid > x / mid) {
                j = mid - 1;
            } else if (mid < x / mid) {
                i = mid + 1;
            } else {
                return mid;
            }
        }
        return j;
    }
}

2. 查找区间

例题34. 在排序数组中查找元素的第一个和最后一个位置

  • 方法一:递归二分查找目标元素,用一个数组存储位置信息,并不断更新最大最小位置信息。
  • 方法二:二分法分别查找左右边界。
class Solution {
    public int[] searchRange(int[] nums, int target) {

        int[] res = new int[2];
        res[0] = getLeft(nums, target);
        res[1] = getRight(nums, target);
        if (res[0] >= nums.length || nums[res[0]] != target) {
            return new int[]{-1, -1};
        }
        return res;
    }

    public int getLeft(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        int mid = 0;
        while (left < right) {
            mid = (left + right) / 2;
            if (target <= nums[mid]) {
                right = mid;
            } else {
            	//一般都是left = mid + 1 而不是 left=mid,否则容易陷入死循环
                left = mid + 1;
            }
        }
        return left;
    }

    public int getRight(int[] nums, int target) {
        int left = 0;
        //这里不减一是为了考虑nums只有一个数时也能进入循环
        int right = nums.length;
        int mid = 0;
        while (left < right) {
            mid = (left + right) / 2;
            if (target < nums[mid]) {
                right = mid;
            } else {    
                left = mid + 1;
            }
        }
        return right - 1;
    }
}

3. 翻转数组查找数字

例题33。按升序排列的整数数组 nums 在 k 处进行了旋转,从中找出目标值所在的位置。数组中的值互不相同

  • 旋转排序数组问题,数值全不相同,有个规律,比较中间数与第一个数的值,如果nums[i] <= nums[mid],左边有序,否则右边有序;
  • 如果左边有序且目标值在左边,则二分查找;左边有序目标值在右边,则比较中间值与右边第一个数的值进行重复判断;
  • 如果右边有序且目标值在右边,则二分查找;右边有序且目标值在左边,则重复判断。
  • 对于数组中的值会重复的情况,如果 nums[i] == nums[mid] 则无法判断哪边有序,可判断 nums[i+1] 与 nums[mid] 的大小关系,以此类推。
  • 此外,无论数组中的值是否会重复,只要其有序二分查找的方法都一样

四. 排序算法

1. 常用排序算法

//冒泡排序,比较相邻元素间的大小,每循环一次确定最后一个值
//平均时间复杂度O(n^2),稳定
for (int i = 0; i < nums.length; i++) {
    for(int j = 0; j < nums.length - i - 1; j++) {
        if(nums[j] > nums [j + 1]) {
            int tmp = nums[j+1];
            nums[j+1] = nums[j];
            nums[j] = tmp;
        }
    }
}
return nums;
//选择排序,将当前元素与后续每一个元素进行比较,每次循环确定第一个值
//O(n^2),不稳定
for (int i = 0; i < nums.length; i++) {
    for (int j = i+1; j < nums.length; j++) {
        if (nums[j] < nums[i]) {
            int tmp = nums[i];
            nums[i] = nums[j];
            nums[j] = tmp;
        }
    }
}
return nums;
//插入排序,将当前元素之前的数组当成已排序数组,从后往前相邻比较并交换
//O(n^2), 稳定 
for (int i = 1; i < nums.length; i++) {
    for (int j = i; j > 0; j--) {
        if (nums[j] < nums[j-1]) {
            int tmp = nums[j];
            nums[j] = nums[j-1];
            nums[j-1] = tmp;
        } else {
            break;
        }
    }
}
return nums;
//归并排序,不断拆分数组直至最小,归并两队已排序数组
//O(nlogn),稳定
int[] tmp = new int[nums.length];
mergeSort(nums, 0, nums.length-1, tmp);
return nums;

public void mergeSort(int[] nums, int l, int r, int[] tmp) {
    if (l >= r) {
        return;
    }
    int m = (l + r) / 2;
    mergeSort(nums, l, m, tmp); //递归不断拆分数组直至最小
    mergeSort(nums, m+1, r, tmp);

    int p = l;
    int q = m+1;
    int i = l;
    //拆分后开始合并有序数组至tmp
    while(p <= m || q <= r) {
        if (q > r || (p <= m && nums[p] <= nums[q])) {
            tmp[i++] = nums[p++];
        } else {
            tmp[i++] = nums[q++];
        }
    }
    i = l;
    while(i <= r) {
        nums[i] = tmp[i];
        i++;
    }
}
//快排,以数组中第一个数为基准,头尾两个指针,比较指针所指元素与基准元素的大小
//每排一次,确定的位置是最后替换元素的位置
//O(nlogn),不稳定
quickSort(0, nums.length-1, nums);
return nums;

public void quickSort(int i, int j, int[] nums) {
    if (i >= j) {
        return;
    }
    int start = i;
    int end = j;
    int tmp = nums[i];
    while (i < j) {
        while (i < j && nums[j] >= tmp) {
            j --;
        }
        nums[i] = nums[j];
        while (i < j && nums[i] <= tmp) {
            i++;
        }
        nums[j] = nums[i];
    }
    nums[i] = tmp;
    quickSort(start, i-1, nums);
    quickSort(i+1, end, nums);
}

2. 快速选择

例题 215. 数组中的第K个最大元素

  • 快速选择,与快排类似,但是无需对元素左右两边都排序。每排完一次可以确定第 i 个元素在数组中的排序;
  • 即可比较 i 与 k 的大小,若 i == k,则返回 nums[i];若 i < k,对右边元素进行快排;若 i > k,对左边元素进行快排。
  • 此外,为避免极端情况时间复杂度为 O(n^2),可以随机获取每轮进行比较的 key 值,不一定取第一个数。

3. 桶排序

例题347 找出数组中前 k 个出现频率最高的元素。

  • 桶排序,桶排序的意思是为每个值设立一个桶,桶内记录这个值出现的次数,然后对桶进行排序。
  • 先遍历数组,用hashmap存储每个元素对应的个数,然后对hashmap的值排序。
  • 对 hashmap 的值排序可以维护一个元素数目为 k 的最小堆,当元素数量小于k时,直接插入,大于 k 时比较插入值与堆顶元素的大小,将小的弹出。
  • 最小堆可用优先队列实现。
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
        }
		//优先队列,按map值进行排序,这里用最小堆,堆顶存储的是最小元素
        PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((a, b) -> a.getValue() - b.getValue());
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            queue.offer(entry);
            //超过k个元素无需存储在队列中
            if(queue.size() > k) {
                queue.poll();
            }
        }
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            res[i] = queue.poll().getKey();
        }
        return res;
    }
}

4. 三指针排序

例题75. 对三个重复且打乱顺序的数排序。

  • 三指针,一个指针l指向头,一个指针r指向尾;
  • 第三个指针i遍历,遇到0与l交换,此时i 与 l 都可以后移一位,因为交换到头的一定是0;
  • 遇到2与r交换,此时r前移,但是 i 不一定能后移,因为交换后的 i 的值不确定。
public void sortColors(int[] nums) {
        int l = 0;
        int r = nums.length - 1;
        int i = 0;
        while (i <= r) {
            if(nums[i] == 0) {
                swap(nums, i++, l++);     
            } else if (nums[i] == 2) {
                swap(nums, i, r--);
            } else {
                i ++;
            }
        }
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值