文章目录
- 励志
- 常见算法
- 一、剑指 Offer 40. 最小的 k 个数
- 二、剑指 Offer 41. 数据流中的中位数
- 三、剑指 Offer 45. 把数组排成最小的数
- 四、剑指 Offer 61. 扑克牌中的顺子
励志
Money is not life’s report card.
金钱不能用来衡量人生精彩与否。
常见算法
下图所示,为在 「随机乱序」、「接近有序」、「完全倒序」、「少数独特」四类输入数据下,各常见排序算法的排序过程。
排序算法主要可根据 稳定性 、就地性 、自适应性 分类。
- 具有稳定性,即相等元素的相对位置不变化;
- 具有就地性,即不使用额外的辅助空间;
- 具有自适应性,即时间复杂度受元素分布影响;
冒泡排序:
AC模板:
void bubbleSort(int[] nums) {
int N = nums.length;
for (int i = 0; i < N - 1; i++) {
for (int j = 0; j < N - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
}
}
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
优化版的冒泡排序:
void bubbleSort(int[] nums) {
int N = nums.length;
for (int i = 0; i < N - 1; i++) {
boolean flag = false; // 初始化标志位
for (int j = 0; j < N - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = true; // 记录交换元素
}
}
if (!flag) break; // 内循环未交换任何元素,则跳出
}
}
若在某轮「内循环」中未执行任何交换操作,则说明数组已经完成排序
快速排序:
算法思想:哨兵划分
和 递归
哨兵划分:
以数组某个元素(一般选取首元素)为 基准数 ,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。
递归:
对 左子数组 和 右子数组 分别递归执行 哨兵划分
AC模板:
void quickSort(int[] nums, int l, int r) {
// 子数组长度为 1 时终止递归
if (l >= r) return;
// 哨兵划分操作
int i = partition(nums, l, r);
// 递归左(右)子数组执行哨兵划分
quickSort(nums, l, i - 1);
quickSort(nums, i + 1, r);
}
int partition(int[] nums, int l, int r) {
// 以 nums[l] 作为基准数
int i = l, j = r;
while (i < j) {
// 因为我们将最左边的数字作为基准
// 所以先j--,然后i++(先移动j,后移动i)
while (i < j && nums[j] >= nums[l]) j--;
while (i < j && nums[i] <= nums[l]) i++;
swap(nums, i, j);
}
swap(nums, i, l);
return i;
}
void swap(int[] nums, int i, int j) {
// 交换 nums[i] 和 nums[j]
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
快速排序的常见优化手段有「Tail Call」和「随机基准数」两种。
Tail Call:
控制递归深度,每次递归短数组
void quickSort(int[] nums, int l, int r) {
// 子数组长度为 1 时终止递归
while (l < r) {
// 哨兵划分操作
int i = partition(nums, l, r);
// 仅递归至较短子数组,控制递归深度
if (i - l < r - i) {
quickSort(nums, l, i - 1);
l = i + 1;
} else {
quickSort(nums, i + 1, r);
r = i - 1;
}
}
}
随机基准数:
每轮在子数组中找一个随机基准数
int partition(int[] nums, int l, int r) {
// 在闭区间 [l, r] 随机选取任意索引,并与 nums[l] 交换
int ra = (int)(l + Math.random() * (r - l + 1));
swap(nums, l, ra);
// 以 nums[l] 作为基准数
int i = l, j = r;
while (i < j) {
while (i < j && nums[j] >= nums[l]) j--;
while (i < j && nums[i] <= nums[l]) i++;
swap(nums, i, j);
}
swap(nums, i, l);
return i;
}
归并排序:
算法思想: 分而治之
分:不断将数组从 中点位置 划分开
治:将 左右两个较短排序数组 合并为 一个较长排序数组
AC模板:
void mergeSort(int[] nums, int l, int r) {
// 终止条件
if (l >= r) return;
// 递归划分
int m = (l + r) / 2;
mergeSort(nums, l, m);
mergeSort(nums, m + 1, r);
// 合并子数组
int[] tmp = new int[r - l + 1]; // 暂存需合并区间元素
for (int k = l; k <= r; k++)
tmp[k - l] = nums[k];
int i = 0, j = m - l + 1; // 两指针分别指向左/右子数组的首个元素
for (int k = l; k <= r; k++) { // 遍历合并左/右子数组
if (i == m - l + 1)
nums[k] = tmp[j++];
else if (j == r - l + 1 || tmp[i] <= tmp[j])
nums[k] = tmp[i++];
else {
nums[k] = tmp[j++];
}
}
}
一、剑指 Offer 40. 最小的 k 个数
题:
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
解:
解题思路:快速排序
AC代码:
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int len = arr.length;
if(k >= len) return arr;
return quick_sort(arr, k, 0, len - 1);
}
int[] quick_sort(int[] nums, int k, int left, int right){
int i = left, j = right;
while(i < j){
while(i < j && nums[j] >= nums[left]) j--;
while(i < j && nums[i] <= nums[left]) i++;
if(i < j) swap(nums, i, j);
}
swap(nums, left, i);
if(k < i) return quick_sort(nums, k, left, i-1);
else if(k > i) return quick_sort(nums, k, i+1, right);
return Arrays.copyOf(nums, k);
}
void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
二、剑指 Offer 41. 数据流中的中位数
题:
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
- void addNum(int num) - 从数据流中添加一个整数到数据结构中。
- double findMedian() - 返回目前所有元素的中位数。
示例 1:
输入
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
限制:
最多会对 addNum、findMedian 进行 50000 次调用。
解:
解题思路: 大顶堆
+ 小顶堆
AC代码:
class MedianFinder {
Queue<Integer> A, B;
/** initialize your data structure here. */
public MedianFinder() {
A = new PriorityQueue<>(); // 大顶堆
B = new PriorityQueue<>((x, y) -> (y - x)); // 小顶堆
}
public void addNum(int num) {
if(A.size() != B.size()){ // A少B多
A.add(num);
B.add(A.poll());
}else{
B.add(num);
A.add(B.poll());
}
}
public double findMedian() {
return A.size() != B.size() ? A.peek() : (A.peek() + B.peek())/2.0;
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
三、剑指 Offer 45. 把数组排成最小的数
题:
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
输入: [10,2]
输出: "102"
示例 2:
输入: [3,30,34,5,9]
输出: "3033459"
提示:
0 < nums.length <= 100
说明:
- 输出结果可能非常大,所以你需要返回一个字符串而不是整数
- 拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0
解:
解题思路: 快速排序
AC代码:
class Solution {
public String minNumber(int[] nums) {
int len = nums.length;
String[] strs = new String[len]; // 数组变字符串数组
for(int i = 0; i < len; i++) strs[i] = String.valueOf(nums[i]);
quickSort(strs, 0, len-1);
StringBuilder res = new StringBuilder(); // 字符串数组转字符串
for(String str : strs) res.append(str);
return res.toString();
}
void quickSort(String[] strs, int l, int r){
if(l >= r) return;
int i = l, j = r;
while(i < j){
while(i < j && (strs[l] + strs[j]).compareTo(strs[j] + strs[l]) <= 0) j--; // l+j < j+l
while(i < j && (strs[i] + strs[l]).compareTo(strs[l] + strs[i]) <= 0) i++; // i+l < l+i
if(i < j) swap(strs, i, j);
}
swap(strs, i, l);
quickSort(strs, l, i-1);
quickSort(strs, i+1, r);
}
void swap(String[] strs, int i, int j){
String temp = strs[i];
strs[i] = strs[j];
strs[j] = temp;
}
}
另解:内置函数
class Solution {
public String minNumber(int[] nums) {
String[] strs = new String[nums.length];
for(int i = 0; i < nums.length; i++)
strs[i] = String.valueOf(nums[i]);
Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));
StringBuilder res = new StringBuilder();
for(String s : strs)
res.append(s);
return res.toString();
}
}
四、剑指 Offer 61. 扑克牌中的顺子
题:
从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
示例 1:
输入: [1,2,3,4,5]
输出: True
示例 2:
输入: [0,0,1,2,5]
输出: True
限制:
数组长度为 5
数组的数取值为 [0, 13] .
解:
解题思路:集合Set
+ 遍历
AC代码:
class Solution {
public boolean isStraight(int[] nums) {
Set<Integer> repeat = new HashSet<>();
int max = 0, min = 14;
for(int num : nums){
if(num == 0) continue; // 跳过大小王
max = Math.max(num, max);
min = Math.min(num, min);
if(repeat.contains(num)) return false; // 顺子不能有一样的牌
repeat.add(num);
}
return max -min < 5;
}
}
- 时间复杂度 O(1) : 本题中给定牌数量 N≡5 ;遍历数组使用 O(N) = O(5) = O(1)时间。
- 空间复杂度 O(1) : 用于判重的辅助 Set 使用 O(N) = O(1) 额外空间。
解题思路:排序
+遍历
AC代码:
class Solution {
public boolean isStraight(int[] nums) {
int joker = 0; // 大小王的数量, 顺子起始下标
Arrays.sort(nums);
for(int i = 0; i < 4; i++){
if(nums[i] == 0){ // 碰到大小王
joker++;
continue;
}
if(nums[i] == nums[i+1]) return false; // 存在重复元素
}
return nums[4] - nums[joker] < 5;
}
}