【算法】一文刷完LeetCode(Java语言)全部典型题(持续更新)

1 题型分类

在这里插入图片描述

2 贪心算法

2.1 算法概念

贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择
贪心算法一般按如下步骤进行:
①建立数学模型来描述问题。
②把求解的问题分成若干个子问题。
③对每个子问题求解,得到子问题的局部最优解。
④把子问题的解局部最优解合成原来解问题的一个解。

2.2 分配问题

  1. 分发饼干
    假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
    对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
    示例 1:
    输入: g = [1,2,3], s = [1,1]
    输出: 1
    解释:
    你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
    虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
    所以你应该输出1。
    示例 2:
    输入: g = [1,2], s = [1,2,3]
    输出: 2
    解释:
    你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
    你拥有的饼干数量和尺寸都足以让所有孩子满足。
    所以你应该输出2.

题解:
优先满足胃口最小的孩子,把大于等于这个孩子饥饿度g[i],且最小的饼干s[j]分配给这个孩子。

class Solution {
   
    public int findContentChildren(int[] g, int[] s) {
   
        Arrays.sort(g);
        Arrays.sort(s);
        int i = 0, j = 0;
        while (i < g.length && j < s.length) {
   
            if (g[i] <= s[j]) ++i;
            ++j;
        }
        return i;
    }
}

执行用时: 8 ms 内存消耗: 39.2 MB
时间复杂度:O(mlogm + nlogn),m 和 n 为数组 g 和 s 的长度,排序两个数组的时间复杂度为 mlogm + nlogn,遍历两个数组的时间复杂度为 m + n ,总时间复杂度为O(mlogm + nlogn);
空间复杂度:O(logm + logn),需要额外的空间开销 logm + logn

官方题解:

class Solution {
   
    public int findContentChildren(int[] g, int[] s) {
   
        Arrays.sort(g);
        Arrays.sort(s);
        int numOfChildren = g.length, numOfCookies = s.length;
        int count = 0;
        for (int i = 0, j = 0; i < numOfChildren && j < numOfCookies; i++, j++) {
   
            while (j < numOfCookies && g[i] > s[j]) {
   
                j++;//饼干尺寸小于孩子胃口把饼干序号加1
            }
            if (j < numOfCookies) {
   
                count++;
            }
        }
        return count;
    }
}

执行用时: 9 ms 内存消耗: 39.4 MB

  1. 分发糖果
    老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。
    你需要按照以下要求,帮助老师给这些孩子分发糖果:
    每个孩子至少分配到 1 个糖果。
    评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。
    那么这样下来,老师至少需要准备多少颗糖果呢?
    示例 1:
    输入:[1,0,2]
    输出:5
    解释:你可以分别给这三个孩子分发 2、1、2 颗糖果。

题解:
把所有孩子的糖果数初始化为 1,先从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1;再从右往左遍历一遍,如果左边孩子的评分比右边的高,且左边孩子当前的糖果数不大于右边孩子的糖果数,则左边孩子的糖果数更新为右边孩子的糖果数加 1。

class Solution {
   
    public int candy(int[] ratings) {
   
        int n = ratings.length;
        if (n < 2) {
   //只有1个孩子情况直接返回1
            return n;
        }
        int[] num = new int[n];//创建糖果数组初始值默认为0,返回值要加上n
        for (int i = 1; i < n; ++i) {
   //从左往右遍历
            if (ratings[i] > ratings[i-1]) {
   //如果右侧孩子评分大于左侧
                num[i] = num[i-1] + 1;//右侧孩子糖果加1
            }
        }
        for (int i = n - 1; i > 0; --i) {
   //从右往左遍历
            if (ratings[i] < ratings[i-1]) {
   //如果右侧孩子评分小于左侧
                num[i-1] = Math.max(num[i-1], num[i] + 1);//左侧糖果为原数和右侧加1的较大值
            }
        }
        return Arrays.stream(num).sum() + n;//数组num求和并加上初始糖果数n
    }
}

执行用时: 7 ms 内存消耗: 39.2 MB(在for循环中++i和i++的结果是一样的都在循环一次之后执行,但++i性能更好)

官方题解:

class Solution {
   
    public int candy(int[] ratings) {
   
        int n = ratings.length;
        int[] left = new int[n];
        for (int i = 0; i < n; i++) {
   
            if (i > 0 && ratings[i] > ratings[i - 1]) {
   
                left[i] = left[i - 1] + 1;
            } else {
   
                left[i] = 1;
            }
        }
        int right = 0, ret = 0;
        for (int i = n - 1; i >= 0; i--) {
   
            if (i < n - 1 && ratings[i] > ratings[i + 1]) {
   
                right++;
            } else {
   
                right = 1;
            }
            ret += Math.max(left[i], right);
        }
        return ret;
    }
}

执行用时: 3 ms 内存消耗: 39.5 MB

  1. 种花问题
    假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
    给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
    示例 1:
    输入:flowerbed = [1,0,0,0,1], n = 1
    输出:true

题解:
遍历花坛中的元素,满足种花条件则n减1,返回判断n是否<=0

class Solution {
   
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
   
        for (int i = 0; i < flowerbed.length; ++i) {
   
            if (flowerbed[i] == 0 && (i + 1 == flowerbed.length || flowerbed[i + 1] == 0)) {
   
                n--;
                i++;
            } else if (flowerbed[i] == 1) {
   
                i++;
            }
        }
        return n <= 0;
    }
}

执行用时: 1 ms 内存消耗: 40 MB

官方题解:
贪心策略:在不打破种植规则的情况下种入尽可能多的花,然后判断可以种入的花的最多数量是否大于或等于 n。
实现方法:
维护 prev 表示上一朵已经种植的花的下标位置,初始时prev=−1,表示尚未遇到任何已经种植的花。
从左往右遍历数组flowerbed,当遇到flowerbed[i]=1时根据previ 的值计算上一个区间内可以种植花的最多数量,然后令prev=i,继续遍历数组 flowerbed 剩下的元素。
遍历数组flowerbed 结束后,根据数组prev 和长度 m 的值计算最后一个区间内可以种植花的最多数量。
判断整个花坛内可以种入的花的最多数量是否大于或等于 n

class Solution {
   
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
   
        int count = 0;
        int m = flowerbed.length;
        int prev = -1;
        for (int i = 0; i < m; i++) {
   
            if (flowerbed[i] == 1) {
   
                if (prev < 0) {
   
                    count += i / 2;
                } else {
   
                    count += (i - prev - 2) / 2;
                }
                prev = i;
            }
        }
        if (prev < 0) {
   
            count += (m + 1) / 2;
        } else {
   
            count += (m - prev - 1) / 2;
        }
        return count >= n;
    }
}

执行用时: 1 ms 内存消耗: 39.8 MB

2.3 区间问题

  1. 无重叠区间
    给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
    注意:
    可以认为区间的终点总是大于它的起点。
    区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
    示例 1:
    输入: [ [1,2], [2,3], [3,4], [1,3] ]
    输出: 1
    解释: 移除 [1,3] 后,剩下的区间没有重叠。

题解:
贪心策略:优先保留结尾小且不相交的区间。
实现方法:先把区间按照结尾的大小进行增序排序,每次选择结尾最小且和前一个选择的区间不重叠的区间。

class Solution {
   
    public int eraseOverlapIntervals(int[][] intervals) {
   
        if (intervals.length == 0) {
   
            return 0;
        }
        Arrays.sort(intervals, new Comparator<int[]>() {
   //将数组集合按第二个元素大小排序
            public int compare(int[] interval1, int[] interval2) {
   
                return interval1[1] - interval2[1];
            }
        });
        int n = intervals.length;
        int total = 0, prev = intervals[0][1];//预置为数组中的第二个元素
        for (int i = 1; i < n; ++i) {
   
            if (intervals[i][0] < prev) {
   //该数组的第一个元素与前一个数组的第二元素进行比较
                ++total;//如果prev大,区间有重叠,移除该区间
            } else {
   
                prev = intervals[i][1];//将prev置为第i个数组的第二个元素
            }
        }
    return total;
    }
}

执行用时: 3 ms 内存消耗: 38.1 MB

官方题解:

class Solution {
   
    public int eraseOverlapIntervals(int[][] intervals) {
   
        if (intervals.length == 0) {
   
            return 0;
        }
        
        Arrays.sort(intervals, new Comparator<int[]>() {
   
            public int compare(int[] interval1, int[] interval2) {
   
                return interval1[1] - interval2[1];
            }
        });

        int n = intervals.length;
        int right = intervals[0][1];
        int ans = 1;
        for (int i = 1; i < n; ++i) {
   
            if (intervals[i][0] >= right) {
   
                ++ans;
                right = intervals[i][1];
            }
        }
        return n - ans;
    }
}

执行用时: 3 ms 内存消耗: 38.1 MB

  1. 用最少数量的箭引爆气球
    在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。
    一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
    给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
    示例 1:
    输入:points = [[10,16],[2,8],[1,6],[7,12]]
    输出:2
    解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球

题解:
贪心策略:相当于435题的反面,尽量使区间重叠,注意区间可为负数
实现方法:先把区间按照结尾的大小进行增序排序 (区间可以为负数,注意此处排序方式),每次选择结尾最小且和前一个选择的区间重叠的区间。

class Solution {
   
    public int findMinArrowShots(int[][] points) {
   
        if (points.length == 0) {
   
            return 0;
        }
        Arrays.sort(points, new Comparator<int[]> () {
   
            public int compare (int[] point1, int[] point2) {
   
                return point1[1] > point2[1] ? 1 : -1;
            }
        });
        int n = points.length;
        int total = n, prev = points[0][1];
        for (int i = 1; i < n; ++i) {
   
            if (points[i][0] <= prev) {
   
                --total;
            } else {
   
                prev = points[i][1];
            }
        }
        return total;
    }
}

执行用时: 19 ms 内存消耗: 45.4 MB

官方题解:

class Solution {
   
    public int findMinArrowShots(int[][] points) {
   
        if (points.length == 0) {
   
            return 0;
        }
        Arrays.sort(points, new Comparator<int[]>() {
   
            public int compare(int[] point1, int[] point2) {
   
                if (point1[1] > point2[1]) {
   
                    return 1;
                } else if (point1[1] < point2[1]) {
   
                    return -1;
                } else {
   
                    return 0;
                }
            }
        });
        int pos = points[0][1];
        int ans = 1;
        for (int[] balloon: points) {
   
            if (balloon[0] > pos) {
   
                pos = balloon[1];
                ++ans;
            }
        }
        return ans;
    }
}

执行用时: 18 ms 内存消耗: 45.7 MB

3 双指针

3.1 算法概念

双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。

3.2 两数求和

  1. 两数之和 II - 输入有序数组
    给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target 。
    函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。
    你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
    示例 1:
    输入:numbers = [2,7,11,15], target = 9
    输出:[1,2]
    解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2.

题解:
采用方向相反的双指针来寻找这两个数字,一个初始指向最小的元素l,即数组最左边,向右遍历;一个初始指向最大的元素r,即数组最右边,向左遍历。

class Solution {
   
    public int[] twoSum(int[] numbers, int target) {
   
        int l = 0, r = numbers.length - 1, sum;//新建变量l、r、sum
        while (l < r) {
   
            sum = numbers[l] + numbers[r];
            if (sum == target) break;
            if (sum < target) ++l;//和小于sum时把较小数增大
            else --r;//反之把较大数减小
        }
        return new int[]{
   l + 1, r + 1};//返回新建数组元素指针从0开始需要加1
    }
}

执行用时: 1 ms 内存消耗: 38.6 MB

官方题解:

class Solution {
   
    public int[] twoSum(int[] numbers, int target) {
   
        int low = 0, high = numbers.length - 1;
        while (low < high) {
   
            int sum = numbers[low] + numbers[high];
            if (sum == target) {
   
                return new int[]{
   low + 1, high + 1};
            } else if (sum < target) {
   
                ++low;
            } else {
   
                --high;
            }
        }
        return new int[]{
   -1, -1};
    }
}

执行用时: 1 ms 内存消耗: 38.8 MB

  1. 平方数之和
    给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c 。
    示例 1:
    输入:c = 5
    输出:true
    解释:1 * 1 + 2 * 2 = 5

题解:
使用方向相反的两个指针,假设这两个数存在,最大范围为0~sqrt©,指针l指向最左侧0,向右遍历,指针r指向sqrt©取整,向左遍历。

class Solution {
   
    public boolean judgeSquareSum(int c) {
   
        int l = 0, sum;
        int r = (int) Math.sqrt(c);
        while (l <= r) {
   
            sum = l * l + r * r;
            if (sum == c) return true;
            if (sum < c) ++l;
            else --r;
        }
        return false;
    }
}

执行用时: 2 ms 内存消耗: 35.1 MB

官方题解:

class Solution {
   
    public boolean judgeSquareSum(int c) {
   
        long left = 0;
        long right = (long) Math.sqrt(c);
        while (left <= right) {
   
            long sum = left * left + right * right;
            if (sum == c) {
   
                return true;
            } else if (sum > c) {
   
                right--;
            } else {
   
                left++;
            }
        }
        return false;
    }
}

执行用时: 4 ms 内存消耗: 35.2 MB

  1. 验证回文字符串 Ⅱ
    给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
    示例 1:
    输入: “aba”
    输出: True

题解:
1.创建两个指针,左指针l指向0,向右遍历,右指针r指向最右,向左遍历;
2.比较两个字符是否相等,返回相应的三种情况;
3.创建新方法isPalindrome()判断删去一个字符后的字符串是否时回文字符串。

class Solution {
   
    public boolean validPalindrome(String s) {
   
        int l = 0, r = s.length() - 1;
        while (l < r && s.charAt(l) == s.charAt(r)) {
   
            ++l;
            --r;
        }
        //返回三种情况,第一种s本身即为回文字符串,第二种为判断删去一个字符的情况
        return l >= r || isPalindrome(s, l, r - 1) || isPalindrome(s, l + 1, r); 
    }
    public boolean isPalindrome(String s, int l, int r) {
   
        while (l < r && s.charAt(l) == s.charAt(r)) {
   
            ++l;
            --r;
        }
        return l >= r;
    }
}

执行用时: 7 ms 内存消耗: 38.5 MB

官方题解:

class Solution {
   
    public boolean validPalindrome(String s) {
   
        int low = 0, high = s.length() - 1;
        while (low < high) {
   
            char c1 = s.charAt(low), c2 = s.charAt(high);
            if (c1 == c2) {
   
                ++low;
                --high;
            } else {
   
                return validPalindrome(s, low, high - 1) || validPalindrome(s, low + 1, high);
            }
        }
        return true;
    }

    public boolean validPalindrome(String s, int low, int high) {
   
        for (int i = low, j = high; i < j; ++i, --j) {
   
            char c1 = s.charAt(i), c2 = s.charAt(j);
            if (c1 != c2) {
   
                return false;
            }
        }
        return true;
    }
}

执行用时: 7 ms 内存消耗: 39 MB

3.3 归并有序数组

  1. 合并两个有序数组
    给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
    初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。
    示例 1:
    输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
    输出:[1,2,2,3,5,6]

题解:
把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。直接利用m和n当作两个数组的指针,在创建一个pos用于确定新数组nums1的指针位置。

class Solution {
   
    public void merge(int[] nums1, int m, int[] nums2, int n) {
   
        int pos = m-- + n-- -1;//pos起始位置为m+n-1
        while (m >= 0 && n >= 0) {
   
            nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];//两数组中较大的元素加入到nums1中
        }
        while (n >= 0) {
   //nums2还有剩余数组
            nums1[pos--] = nums2[n--];
        }
    }
}

执行用时: 0 ms 内存消耗: 38.8 MB

官方题解:

class Solution {
   
    public void merge(int[] nums1, int m, int[] nums2, int n) {
   
        int p1 = 0, p2 = 0;
        int[] sorted = new int[m + n];
        int cur;
        while (p1 < m || p2 < n) {
   
            if (p1 == m) {
   
                cur = nums2[p2++];
            } else if (p2 == n) {
   
                cur = nums1[p1++];
            } else if (nums1[p1] < nums2[p2]) {
   
                cur = nums1[p1++];
            } else {
   
                cur = nums2[p2++];
            }
            sorted[p1 + p2 - 1] = cur;
        }
        for (int i = 0; i != m + n; ++i) {
   
            nums1[i] = sorted[i];
        }
    }
}

执行用时: 0 ms 内存消耗: 38.6 MB

  1. 通过删除字母匹配到字典里最长单词
    给你一个字符串 s 和一个字符串数组 dictionary 作为字典,找出并返回字典中最长的字符串,该字符串可以通过删除 s 中的某些字符得到。
    如果答案不止一个,返回长度最长且字典序最小的字符串。如果答案不存在,则返回空字符串。
    示例 1:
    输入:s = “abpcplea”, dictionary = [“ale”,“apple”,“monkey”,“plea”]
    输出:“apple”

3.4 快慢指针

  1. 环形链表 II
    给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
    为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
    说明:不允许修改给定的链表。
    进阶:
    你是否可以使用 O(1) 空间解决此题?
    示例 1:
    输入:head = [3,2,0,-4], pos = 1
    输出:返回索引为 1 的链表节点
    解释:链表中有一个环,其尾部连接到第二个节点。在这里插入图片描述
    题解:
    给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步, slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
   
    public ListNode detectCycle(ListNode head) {
   
        ListNode slow = head, fast = head;//创建快慢指针
        //判断是否存在环路
        do {
   
            if (fast == null || fast.next == null) return null;//不存在结点或只有一个结点返回null
            fast = fast.next.next;//fast移动两个位置
            slow = slow.next; //slow移动一个位置
        } while (fast != slow);//不断循环当快慢指针不能相遇则不存在环路跳出循环
        fast = head;//第一次相遇将fast指针移动到链表开头
        //查找环路结点,快慢指针都只移动一个位置第二次相遇位置即为结点
        while (fast != slow) {
   
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

执行用时: 0 ms 内存消耗: 38.7 MB

官方题解:

public class Solution {
   
    public ListNode detectCycle(ListNode head) {
   
        if (head == null) {
   
            return null;
        }
        ListNode slow = head, fast = head;
        while (fast != null) {
   
            slow = slow.next;
            if (fast.next != null) {
   
                fast = fast.next.next;
            } else {
   
                return null;
            }
            if (fast == slow) {
   
                ListNode ptr = head;
                while (ptr != slow) {
   
                    ptr = ptr.next;
                    slow = slow.next;
                }
                return ptr;
            }
        }
        return null;
    }
}

执行用时: 0 ms 内存消耗: 38.7 MB

3.5 滑动窗口

  1. 最小覆盖子串
    给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
    注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
    示例 1:
    输入:s = “ADOBECODEBANC”, t = “ABC”
    输出:“BANC”

题解:
1.遍历字符串t并记录需要的字符及其个数,用数组need映射;
2.在字符串s中建立滑动窗口,左边界l,有边界r,不断增加r使滑动窗口增大直到包含了t中的所有元素,need中所有元素<=0且count也为0;
3.不断增加l减小滑动窗口直到碰到第一个必须包含的元素,记录此时的长度;
4.再增加i,重复上述步骤直到滑动窗口再一次满足要求。

class Solution {
   
    public String minWindow(String s, String t) {
   
    	//特殊判空情况
        if (s == null || s.length() == 0 || t == null || t.length() == 0){
   
            return "";
        }
        //用长度128的数组need映射字符
        int[] need = new int[128];
        //遍历字符串t,记录需要的字符的个数
        for (int i = 0; i < t.length(); i++) {
   
            need[t.charAt(i)]++;
        }
        //l是当前左边界,r是当前右边界,size记录窗口大小,count是需求的字符个数,start是最小覆盖串开始的index
        int l = 0, r = 0, size = Integer.MAX_VALUE, count = t.length(), start = 0;
        //遍历s的所有字符
        while (r < s.length()) {
   
            if (need[s.charAt(r)] > 0) {
   
                count--;
            }
            need[s.charAt(r)]--;//把右边的字符加入窗口,s中的字符减少1
            if (count == 0) {
   //窗口中已经包含所有字符
                while (l < r && need[s.charAt(l)] < 0) {
   
                    need[s.charAt(l)]++;//释放右边移动出窗口的字符
                    l++;//指针右移
                }
                if (r - l + 1 < size) {
   //不能右移时候挑战最小窗口大小,更新最小窗口开始的start
                    size = r - l + 1;
                    start = l;//记录下最小值时候的开始位置,最后返回覆盖串时候会用到
                }
                //l向右移动后窗口肯定不能满足了 重新开始循环
                need[s.charAt(l)]++;
                l++;
                count++;
            }
            r++;
        }
        return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
    }
}

执行用时: 5 ms 内存消耗: 38.4 MB

官方题解:

class Solution {
   
    Map<Character, Integer> ori = new HashMap<Character, Integer>();
    Map<Character, Integer> cnt = new HashMap<Character, Integer>();

    public String minWindow(String s, String t) {
   
        int tLen = t.length();
        for (int i = 0; i < tLen; i++) {
   
            char c = t.charAt(i);
            ori.put(c, ori.getOrDefault(c, 0) + 1);
        }
        int l = 0, r = -1;
        int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;
        int sLen = s.length();
        while (r < sLen) {
   
            ++r;
            if (r < sLen && ori.containsKey(s.charAt(r))) {
   
                cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
            }
            while (check() && l <= r) {
   
                if (r - l + 1 < len) {
   
                    len = r - l + 1;
                    ansL = l;
                    ansR = l + len;
                }
                if (ori.containsKey(s.charAt(l))) {
   
                    cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
                }
                ++l;
            }
        }
        return ansL == -1 ? "" : s.substring(ansL, ansR);
    }

    public boolean check() {
   
        Iterator iter = ori.entrySet().iterator(); 
        while (iter.hasNext()) {
    
            Map.Entry entry = (Map.Entry) iter.next(); 
            Character key = (Character) entry.getKey(); 
            Integer val = (Integer) entry.getValue(); 
            if (cnt.getOrDefault(key, 0) < val) {
   
                return false;
            }
        } 
        return true;
    }
}

执行用时: 118 ms 内存消耗: 39 MB
时间复杂度:最坏情况下左右指针对 s 的每个元素各遍历一遍,哈希表中对 s 中的每个元素各插入、删除一次,对 t 中的元素各插入一次。每次检查是否可行会遍历整个 t 的哈希表,哈希表的大小与字符集的大小有关,设字符集大小为 C,则渐进时间复杂度为 O(C⋅∣s∣+∣t∣)。
空间复杂度:这里用了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,我们设字符集大小为 C ,则渐进空间复杂度为 O( C )。

4 二分查找

4.1 算法概念

二分查找(Binary Search)也叫作折半查找。二分查找有两个要求,一个是数列有序,另一个是数列使用顺序存储结构(比如数组)。长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。

4.2 开平方

  1. x 的平方根
    实现 int sqrt(int x) 函数。
    计算并返回 x 的平方根,其中 x 是非负整数。
    由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
    示例 1:
    输入: 4
    输出: 2

题解:
将开方转化为给定一个非负整数x,求 f ® = r^2 − x = 0 的解。
单独考虑x == 0的情况,然后再[1,x]区间内用二分查找法。

class Solution {
   
    public int mySqrt(int x) {
   
        if (x == 0) return x;
        int l = 1, r = x, mid, sqrt;
        while (l <= r) {
   
            mid = l + (r - l) /2;
            sqrt = x / mid;
            if (sqrt == mid) {
   
                return mid;
            } else if (mid > sqrt) {
   
                r = mid - 1;
            } else {
   
                l = mid + 1;
            }
        }
        return r;
    }
}

执行用时: 1 ms 内存消耗: 35.4 MB

官方题解:


class Solution {
   
    public int mySqrt(int x) {
   
        int l = 0, r = x, ans = -1;
        while (l <= r) {
   
            int mid = l + (r - l) / 2;
            if ((long) mid * mid <= x) {
   
                ans = mid;
                l = mid + 1;
            } else {
   
                r = mid - 1;
            }
        }
        return ans;
    }
}

执行用时: 1 ms 内存消耗: 35.6 MB

4.3 查找区间

  1. 在排序数组中查找元素的第一个和最后一个位置
    给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
    如果数组中不存在目标值 target,返回 [-1, -1]。
    进阶:
    你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?
    示例 1:
    输入:nums = [5,7,7,8,8,10], target = 8
    输出:[3,4]

题解:
用二分法分别查找数组中的target。

class Solution {
   
    public int[] searchRange(int[] nums, int target) {
   
        if (nums == null || nums.length == 0) return new int[]{
   -1, -1};
        int lower = lower_bound(nums, target);
        int upper = upper_bound(nums, target) - 1;
        if (lower == nums.length || nums[lower] != target) {
   
            return new int[]{
   -1, -1};
        }
        return new int[]{
   lower, upper};
    }
	//二分查找序号低的target
    int lower_bound(int[] nums, int target) {
   
        int l = 0, r = nums.length, mid;
        while (l < r) {
   
            mid = (l + r) / 2;
            if (nums[mid] >= target) {
   
                r = mid;
            } else {
   
                l = mid + 1;
            }
        }
        return l;
    }
    //二分查找序号高的target
     int upper_bound(int[] nums, int target) {
   
         int l = 0, r = nums.length, mid;
         while (l < r) {
   
            mid = (l + r) / 2;
            if (nums[mid] > target) {
   
                r = mid;
            } else {
   
                l = mid + 1;
            }
        }
        return l;
     }
}

执行用时: 0 ms 内存消耗: 41.7 MB

官方题解:

class Solution {
   
    public int[] searchRange(int[] nums, int target) {
   
        int leftIdx = binarySearch(nums, target, true);
        int rightIdx = binarySearch(nums, target, false) - 1;
        if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) {
   
            return new int[]{
   leftIdx, rightIdx};
        } 
        return new int[]{
   -1, -1};
    }

    public int binarySearch(int[] nums, int target, boolean lower) {
   
        int left = 0, right = nums.length - 1, ans = nums.length;
        while (left <= right) {
   
            int mid = (left + right) / 2;
            if (nums[mid] > target || (lower && nums[mid] >= target)) {
   
                right = mid - 1;
                ans = mid;
            } else {
   
                left = mid + 1;
            }
        }
        return ans;
    }
}

执行用时: 0 ms 内存消耗: 41.4 MB

  1. 有序数组中的单一元素
    给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
    示例 1:
    输入: [1,1,2,3,3,4,4,8,8]
    输出: 2
    注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。

题解:

class Solution {
   
    public int singleNonDuplicate(int[] nums) {
   
        int l = 0, r = nums.length - 1;
        while (l < r) {
   
            int mid = l + (r - l) / 2;
            //mid为偶数且与后一个元素相等,只出现一次元素在mid右侧
            if (mid % 2 == 0 && nums[mid] == nums[mid + 1]) {
   
                l = mid + 2;
        	//mid为奇数且与前一个元素相等,只出现一次元素在mid右侧
            } else if (mid % 2 == 1 && nums[mid - 1] == nums[mid]) {
   
                l = mid + 1;
            //只出现一次元素在左侧
            } else {
   
                r = mid;
            }
        }
        return nums[l];
    }
}

执行用时: 0 ms 内存消耗: 38.7 MB

官方题解:

class Solution {
   
    public int singleNonDuplicate(int[] nums) {
   
        int lo = 0;
        int hi = nums.length - 1;
        while (lo < hi) {
   
            int mid = lo + (hi - lo) / 2;
            if (mid % 2 == 1) mid--;
            if (nums[mid] == nums[mid + 1]) {
   
                lo = mid + 2;
            } else {
   
                hi = mid;
            }
        }
        return nums[lo];
    }
}

执行用时: 0 ms 内存消耗: 38.5 MB

4.4 旋转数组

  1. 搜索旋转排序数组 II
    已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
    在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
    给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
    示例 1:
    输入:nums = [2,5,6,0,0,1,2], target = 0
    输出:true
    进阶:
    这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素。
    这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

题解:
数组旋转后,仍旧保留部分有序和递增,利用这个特性进行二分查找。
对于当前mid,如果该值<=右侧,那么右侧有序,反之左侧有序。
如果target位于有序区间内,继续对这个区间二分查找,反之对另一半区间二分查找。
因为数组存在重复数字,如果中点和左端的数字相同,我们并不能确定是左区间全部相同,还是右区间完全相同。在这种情况下,我们可以简单地将左端点右移一位,然后继续进行二分查找。

class Solution {
   
    public boolean search(int[] nums, int target) {
   
        int start = 0, end = nums.length - 1;
        while (start <= end) {
   
            int mid = (start + end) / 2;
            //恰好找到target直接返回true
            if (nums[mid] == target) {
   
                return true;
            }
            if (nums[start] == nums[mid]) {
   
            //无法判断增序
                ++start;
            } else if (nums[mid] <= nums[end]) {
   
            //右区间增序
                if (target > nums[mid] && target <= nums[end]) {
   
                    start = mid + 1;
                } else {
   
                    end = mid - 1;
                }
            } else {
   
            //左区间增序
                if (target >= nums[start] && target < nums[mid]) {
   
                    end = mid - 1;
                } else {
   
                    start = mid + 1;
                }
            }
        }
        return false;
    }
}

执行用时: 1 ms 内存消耗: 38.1 MB

官方题解:

class Solution {
   
    public boolean search(int[] nums, int target) {
   
        int n = nums.length;
        if (n == 0) {
   
            return false;
        }
        if (n == 1) {
   
            return nums[0] == target;
        }
        int l = 0, r = n - 1;
        while (l <= r) {
   
            int mid = (l + r) / 2;
            if (nums[mid] == target) {
   
                return true;
            }
            if (nums[l] == nums[mid] && nums[mid] == nums[r]) {
   
                ++l;
                --r;
            } else if (nums[l] <= nums[mid]) {
   
                if (nums[l] <= target && target < nums[mid]) {
   
                    r = mid - 1;
                } else {
   
                    l = mid + 1;
                }
            } else {
   
                if (nums[mid] < target && target <= nums[n - 1]) {
   
                    l = mid + 1;
                } else {
   
                    r = mid - 1;
                }
            }
        }
        return false;
    }
}

执行用时: 1 ms 内存消耗: 37.6 MB

  1. 寻找旋转排序数组中的最小值 II
    已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
    若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
    若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
    注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
    给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
    示例 1:
    输入:nums = [1,3,5]
    输出:1

题解:
当 nums[mid] > nums[right]时,最小值一定在mid右侧,此时i一定满足 mid < i <= right,因此执行 left = mid + 1;
当 nums[mid] < nums[right] 时,最小值一定在mid左侧,此时i一定满足 left < i <= mid,因此执行 right = mid;
当 nums[mid] == nums[right] 时,有重复元素无法判断,因此将最右减-1.

class Solution {
   
    public int findMin(int[] nums) {
   
        int left = 0, right = nums.length - 1;
        while (left < right) {
   
            int mid = (left + right) / 2;
            //最小值在右侧
            if (nums[mid] > nums[right]) left = mid + 1;
            //最小值在左侧
            else if (nums[mid] < nums[right]) right = mid;
            //重复数字无法判断
            else right = right - 1;
        }
        return nums[left];
    }
}

执行用时: 0 ms 内存消耗: 38.1 MB

官方题解:

class Solution {
   
    public int findMin(int[] nums) {
   
        int low = 0;
        int high = nums.length - 1;
        while (low < high) {
   
            int pivot = low + (high - low) / 2;
            if (nums[pivot] < nums[high]) {
   
                high = pivot;
            } else if (nums[pivot] > nums[high]) {
   
                low = pivot + 1;
            } else {
   
                high -= 1;
            }
        }
        return nums[low];
    }
}

执行用时: 1 ms 内存消耗: 38.2 MB

5 排序算法

5.1 排序算法大全

算法分类:
在这里插入图片描述
算法复杂度:
在这里插入图片描述
算法详解:
十大排序算法

1 冒泡排序:

①比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
③针对所有的元素重复以上的步骤,除了最后一个,持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
优化: 增加 flag 变量,遍历一遍后没有发生交换,则数组已经有序,直接跳出循环返回数组。

在这里插入图片描述

public static int[] BubbleSort(int[] arr) {
   
	for (int i = 1; i < arr.length; i++) {
   
		boolean flag = true;
		for (int j = 0; j < arr.length - i; j++) {
   
			if (arr[j] > arr[j + 1]) {
   
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = false;
			}
		}
		if (flag) break;
	}
	return arr;
}

2 选择排序

①首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
②再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
③重复第二步,直到所有元素均排序完毕。
在这里插入图片描述
图中红色为 minIndex ,绿色为 j

public static int[] SelectionSort(int[] arr) {
   
	for (int i = 0; i < arr.length - 1; i++) {
   
		int minIndex = i;
		for (int j = i + 1; j < arr.length; j++) {
   
			if (arr[j]  < arr[minIndex]) {
   
				minIndex = j;
			}
		}
		if (minIndex != i) {
   
			int tmp = arr[minIndex];
			arr[minIndex] = arr[i];
			arr[i] = tmp;
		}
	}
	return arr;
}
			

3 插入排序

①将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
②从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
在这里插入图片描述

public static int[] InsertSort(int[] arr) {
   
	for (int i = 1; i < arr.length; i++) {
   
		int preIndex = i - 1;
		int current = arr[i];
		while (preIndex >= 0 && current < arr[preIndex]) {
   
			arr[preIndex + 1] = arr[preIndex];
			preIndex -= 1;
		}
		arr[preIndex + 1] = current;
	}
	return arr;
}

4 希尔排序
希尔排序是一种插入排序,简单插入排序后改进的高效版本,也称为递减增量排算法

①选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
②按增量序列个数 k,对序列进行 k 趟排序;
③每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
在这里插入图片描述

public static int[] ShellSort(int[] arr) {
   
	int n = arr.length;
	int gap = n / 2;
	while (gap > 0) {
   
		for (i = gap; i < n; i++) {
   
			int preIndex = i - gap;
			int current = arr[i];
			while (preIndex >= 0 && arr[preIndex] > current) {
   
				arr[preIndex + gap] = arr[preIndex];
				preIndex -= gap;
			}
			arr[preIndex + gap] = current;
		}
		gap /= 2;
	}
	return arr;
}

5 归并排序

①申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
②设定两个指针,最初位置分别为两个已经排序序列的起始位置;
③比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
⑥重复步骤 3 直到某一指针达到序列尾;
⑦将另一序列剩下的所有元素直接复制到合并序列尾。
在这里插入图片描述

public static int[] MergeSort(int[] arr) {
   
	if (arr.length <= 1) return arr;
	int middle = length / 2;
	int[] arr_1 = Arrays.copyOfRange(arr, 0, middle);
	int[] arr_2 = Arrays.copyOfRange(arr, middle, arr.length);
	return Merge(MergeSort(arr_1), MergeSort(arr_2));
}
public static int[] Merge(int[] arr_1, int[] arr_2) {
   
	int[] sorted_arr = new int[arr_1.length + arr_2.length];
	int idx = 0, idx_1 = 0, idx_2 = 0;
	while(idx_1 < arr_1.length && idx_2 < arr_2.length) {
   
		if (arr_1[idx_1] < arr_2[idx_2]) {
   
			sorted_arr[idx] = arr_1[idx_1];
			idx_1 += 1;
		} else {
   
			sorted_arr[idx] = arr_2[idx_2];
			idx_2 += 1;
		}
		idx += 1;
	}
	if (idx_1 < arr_1.length) {
   
		while(idx_1 < arr_1.length) {
   
			sorted_arr[idx] = arr_1[idx_1];
			idx_1 += 1;
			idx += 1;
		}
	} else {
   
		while(idx_2 < arr_2.length) {
   
			sorted_arr[idx] = arr_2[idx_2];
			idx_2 += 1;
			idx+= 1;
		}
	}
	return aorted_arr;
}

6 快速排序
冒泡排序基础上的递归分治法。快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。平均期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

①从数列中挑出一个元素,称为 “基准”(pivot);
②重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
③递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
在这里插入图片描述

颜色 参数
黄色 pivot
红色 a[i]
紫色 a[i] > pivot
橙色 已排序
绿色 a[i] < pivot

快速排序

双指针交换法:

public static int[] QuickSort(int[] arr, int left, int right) {
   
	if (left < right) {
   
        int pivotIndex = Partition(arr, left, right);
        Quicksort(arr, left, pivotIndex - 1);
        Quicksort(arr, pivotIndex + 1, right);
    }
    return arr;
}
public static int Partiton(int[] arr, int left, int right) {
   
    int pivot = arr[left];
    int start = left;
    while(left < right) {
   
    	while(left < right && arr[right] >= pivot) right--;
    	while(left < right && arr[left] <= pivot) left++;
    	if (left >= right) break;
    	swap(arr, left, right);
    }
    swap(arr, start, left);
    return left;
}
public static void swap(int[] arr, int i, int j) {
   
	int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

挖坑填数法:

public static int[] QuickSort(int[] arr, int left, int right) {
   
	if (left < right) {
   
        int pivotIndex = Partition(arr, left, right);
        Quicksort(arr, left, pivotIndex - 1);
        Quicksort(arr, pivotIndex + 1, right);
    }
    return arr;
}
public static int Partition(int[] arr, int left, int right) {
   
	int pivot = arr[left];
	while(left < right) {
   
		while(left < right && arr[right] >= pivot) right--;
		if (left < right) arr[left] = arr[right];
		while(left < right && arr[left] <= pivot) left++;
		if (left < right) arr[right] = arr[left];
	}
	arr[left] = pivot;
	return left;
}

归并排序和快速排序区别:
在这里插入图片描述
①归并排序是自下而上的,先处理子问题,然后再合并,将小集合合成大集合,最后实现排序;
②快速排序是由上到下的,先分区,然后再处理子问题。

使用栈实现迭代:
利用栈先进后出的特性实现迭代

public static int[] QuickSort(int[] arr, int left, int right) {
   
	Stack<Integer> stack = new Stack<>();
	stack.push(arr.length - 1);
	stack.push(0);
	while(!stack.isEmpty()) {
   
		int left = stack.pop();
		int right = stack.pop();
		if (left < right) {
   
	        int pivotIndex = Partition(arr, left, right);
	        stack.push(pivotIndex - 1);
	        stack.push(left);
	        stack.push(right);
	        stack.push(pivotIndex + 1);
    	}
	}
    return arr;
}
public static int Partiton(int[] arr, int left, int right) {
   
    int pivot = arr[left];
    int start = left;
    while(left < right) {
   
    	while(left < right && arr[right] >= pivot) right--;
    	while(left < right && arr[left] <= pivot) left++;
    	if (left >= right) break;
    	swap(arr, left, right);
    }
    swap(arr, start, left);
    return left;
}
public static void swap(int[] arr, int i, int j) {
   
	int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

基准值选取优化:
三数取中法:
选取基准值时 left 可能恰好为最大值或最小值,迭代耗时,选取时应尽量避免选取序列的最大或最小值做为基准值。

int mid = left + ((right - left) >> 1);
if (arr[left] > arr[right]) swap(arr, left, right);
if (arr[mid] > arr[right]) swap(arr, mid, right);
if (arr[mid] > arr[left]) swap(arr, mid, left);

7 堆排序
利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

①创建一个堆 H[0……n-1];
②把堆首(最大值)和堆尾互换;
③把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
④重复步骤 2,直到堆的尺寸为 1。
在这里插入图片描述

public static int[] HeapSort(int[] arr) {
   
    // index at the end of the heap
    heapLen = arr.length;
    // build MaxHeap
    buildMaxHeap(arr);
    for (int i = arr.length - 1; i > 0; i--) {
   
        // Move the top of the heap to the tail of the heap in turn
        swap(arr, 0, i);
        heapLen -= 1;
        heapify(arr, 0);
    }
    return arr;
}
private static void heapify(int[] arr, int i) {
   
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    int largest = i;
    if (right < heapLen && arr[right] > arr[largest]) {
   
        largest = right;
    }
    if (left < heapLen && arr[left] > arr[largest]) {
   
        largest = left;
    }
    if (largest != i) {
   
        swap(arr, largest, i);
        heapify(arr, largest);
    }
}
private static void buildMaxHeap(int[] arr) {
   
    for (int i = arr.length / 2 - 1; i >= 0; i--) {
   
        heapify(arr, i);
    }
}
private static void swap(int[] arr, int i, int j) {
   
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

8 计数排序

①找出数组中的最大值maxValue、最小值minValue;
②创建一个新数组countArr,其长度是maxValue - minValue + 1,其元素默认值都为0;
③遍历原数组 arr 中的元素 arr[i],以 arr[i]-minValue作为 countArr 数组的索引,以 arr[i] 的值在 arr 中元素出现次数作为countArr[arr[i] - minValue] 的值;
④对 countArr 数组变形,新元素的值是该元素与前一个元素值的和,即当 i > 1 时 countArr[i] = countArr[i] + countArr[i-1];
⑤创建结果数组 result,长度和原始数组一样。
⑥从后向前遍历原始数组 arr 中的元素 arr[i],使用 arr[i] 减去最小值 minValue 作为索引,在计数数组 countArr 中找到对应的值 countArr[arr[i] - minValue],countArr[arr[i] - minValue] - 1就是 arr[i] 在结果数组 result 中的位置,做完上述这些操作,将countArr[arr[i]-minValue]减小1。
在这里插入图片描述

public static int[] CountingSort(int[] arr) {
   
    if (arr.length < 2) {
   
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int[] countArr = new int[maxValue - minValue + 1];
    int[] result = new int[arr.length];

    for (int i = 0; i < arr.length; i++) {
   
        countArr[arr[i] - minValue] += 1;
    }
    for (int i = 1; i < countArr.length; i++) {
   
        countArr[i] += countArr[i - 1];
    }
    for (int i = arr.length - 1; i >= 0; i--) {
   
        int idx = countArr[arr[i] - minValue] - 1;
        result[idx] = arr[i];
        countArr[arr[i] - minValue] -= 1;
    }
    return result;
}
private static int[] getMinAndMax(int[] arr) {
   
    int maxValue = arr[0];
    int minValue = arr[0];
    for (int i = 0; i < arr.length; i++) {
   
        if (arr[i] > maxValue) {
   
            maxValue = arr[i];
        } else if (arr[i] < minValue) {
   
            minValue = arr[i];
        }
    }
    return new int[] {
    minValue, maxValue };
}

9 桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

①设置一个BucketSize,作为每个桶所能放置多少个不同数值;
②遍历输入数据,并且把数据依次映射到对应的桶里去;
③对每个非空的桶进行排序,可以使用其它排序方法,也可以递归使用桶排序;
④从非空桶里把排好序的数据拼接起来。
在这里插入图片描述

public static List<Integer> BucketSort(List<Integer> arr, int bucket_size) {
   
    if (arr.size() < 2 || bucket_size == 0) {
   
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int bucket_cnt = (maxValue - minValue) / bucket_size + 1;
    List<List<Integer>> buckets = new ArrayList<>();
    for (int i = 0; i < bucket_cnt; i++) {
   
        buckets.add(new ArrayList<Integer>());
    }
    for (int element : arr) {
   
        int idx = (element - minValue) / bucket_size;
        buckets.get(idx).add(element);
    }
    for (int i = 0; i < buckets.size(); i++) {
   
        if (buckets.get(i).size() > 1) {
   
            buckets.set(i, sort(buckets.get(i), bucket_size / 2));
        }
    }
    ArrayList<Integer> result = new ArrayList<>();
    for (List<Integer> bucket : buckets) {
   
        for (int element : bucket) {
   
            result.add(element);
        }
    }
    return result;
}
private static int[] getMinAndMax(List<Integer> arr) {
   
    int maxValue = arr.get(0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值