备注:
时间复杂度O(n^2)
冒泡排序
public class BubbleSort {
private static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
boolean isSwap = false;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
isSwap = true;
}
}
if (!isSwap) {
break;
}
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
} LC283 移动零
题目:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12] 输出: [1,3,12,0,0]
public class Solution {
public void moveZeroes(int[] nums) {
int index = 0;
// 1.非0元素往前放,index记录放的位置
for (int num : nums) {
if (num != 0) {
nums[index++] = num;
}
}
// 2.[index,n-1]放回0
for (int i = index; i < nums.length; i++) {
nums[i] = 0;
}
}
} 剑指45 拼接最小数字
题目:输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
输入: [10,2] 输出: "102"
示例 2:
输入: [3,30,34,5,9] 输出: "3033459"
public class Solution {
public String minNumber(int[] nums) {
// 用list存储String
List<String> list = new ArrayList<>();
for (int num : nums) {
// 将nums中的值转换为String存储进list
list.add(String.valueOf(num));
}
// 拼接数组内所有元素使结果最小,本质上是排序
// 若字符串拼接 a + b > b + a,那么排序上 b < a;
list.sort((a, b) -> (a + b).compareTo(b + a));
// 用res接受结果字符串
StringBuilder res = new StringBuilder();
for (String str : list) {
res.append(str);
}
return res.toString();
}
} 选择排序
public class SelectionSort {
private static void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int minIndex = i;
for (int j = i; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swap(arr,i,minIndex );
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
} LC215 找出第K个最大元素
题目:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 输出: 4
public class Solution {
// 选择排序法:找出K次最大值,返回即可
public int findKthLargest(int[] nums, int k) {
// 外层循环k次
for (int i = 0; i < k; i++) {
// 找最大值就行
int maxIndex = i;
for (int j = i + 1; j < nums.length; j++) {
maxIndex = nums[maxIndex] < nums[j] ? j : maxIndex;
}
swap(nums, i, maxIndex);
}
// 第一次最大值下标=1-1=0;
// 第K次最大值下标=k-1
return nums[k - 1];
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
插入排序
public class InsertionSort {
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i; j - 1 >= 0 && temp < arr[j - 1]; j--) {
// 前面的数比temp都大,每个数都依次往后移
arr[j] = arr[j - 1];
}
// 空出的位置放temp
arr[j] = temp;
}
}
} LC147 对链表进行排序
题目:对链表进行插入排序
示例 1:
输入: 4->2->1->3 输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0 输出: -1->0->3->4->5
分析:[1,2,2,3,2]=>[1,2,2,2,3]
- 初始化:哑结点,last结点指向head,cur结点指向head.next
- cur进行遍历,联想[1,2,2,3,2]=>[1,2,2,2,3]交换的过程
public class Solution {
// 问题:链表的插入排序
public ListNode insertionSortList(ListNode head) {
if (head == null) {
return null;
}
// 哑结点
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
// 依然有序,pre后移
if (pre.val <= cur.val) {
pre = pre.next;
} else {
// 倒序,初始化一个lastPre指向哑结点,从头找<=cur.val的最后一个结点的前一个结点
ListNode lastPre = dummyNode;
while (lastPre.next.val <= cur.val) {
lastPre = lastPre.next;
}
// 交换lastPre,pre,cur
pre.next = cur.next;
cur.next = lastPre.next;
lastPre.next = cur;
}
// cur移动到pre后面
cur = pre.next;
}
return dummyNode.next;
}
} 时间复杂度O(nlogn)
希尔排序
希尔排序是对插入排序的一种改进尝试,每次基于一定间隔的的两数进行排序,直到间隔为1再进行插入排序。
在合适步长下,为什么希尔排序可以将时间复杂度降到O(nlogn)?
- 希尔排序是按照间隔来插入排序,比直接插入排序相邻两个比较消除的逆序多会更多
public class ShellSort {
// 希尔排序
public static void shellSort(int[] arr) {
// 步长step
int step = arr.length / 2;
// 插入排序最低就是步长为1
while (step >= 1) {
// 以step为距离进行插入排序
for (int i = step; i < arr.length; i += step) {
int temp = arr[i];
int j;
for (j = i; j - step >= 0 && temp < arr[j - step]; j -= step) {
arr[j] = arr[j - step];
}
arr[j] = temp;
}
step /= 2;
}
}
// 插入排序:最后一步步长是1,不是0
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i; j - 1 >= 0 && temp < arr[j - 1]; j--) {
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
} LC506 相对名次
题目:给出 N 名运动员的成绩,找出他们的相对名次并授予前三名对应的奖牌。前三名运动员将会被分别授予 “金牌”,“银牌” 和“ 铜牌”("Gold Medal", "Silver Medal", "Bronze Medal")。
(注:分数越高的选手,排名越靠前。)
示例:
输入: [5, 4, 3, 2, 1]
输出: ["Gold Medal", "Silver Medal", "Bronze Medal", "4", "5"]
解释: 前三名运动员的成绩为前三高的,因此将会分别被授予 “金牌”,“银牌”和“铜牌” ("Gold Medal", "Silver Medal" and "Bronze Medal").
余下的两名运动员,我们只需要通过他们的成绩计算将其相对名次即可。 public class Solution {
// 排序法+二分查找排名
public String[] findRelativeRanks(int[] score) {
int n = score.length;
int[] copy = new int[n];
System.arraycopy(score, 0, copy, 0, n);
Arrays.sort(copy);
String[] res = new String[n];
for (int i = 0; i < n; i++) {
// 排名+当前排序数组中索引 = 数组长度
int rank = n - Arrays.binarySearch(copy, score[i]);
switch (rank) {
case 1:
res[i] = ("Gold Medal");
break;
case 2:
res[i] = ("Silver Medal");
break;
case 3:
res[i] = ("Bronze Medal");
break;
default:
// 不是奖牌排名,就放入排名
res[i] = String.valueOf(rank);
}
}
return res;
}
} 堆排序
- 堆排序、快速排序、归并排序是面试中最高频的3个面试
public class HeapSort {
public static void heapSort(int[] arr) {
if (arr.length < 2) {
return;
}
// 从小到大排序,是建立大根堆
// 从大到小排序,是建立小根堆
// 1.自底向上将原数组转换成最大堆,arr[0]是整个数组最大值
buildMaxHeap(arr);
// 2.依次交换数组首位(最大值)和数组末尾,然后对剩下的[0,n-1...)再进行堆化操作
for (int j = arr.length - 1; j >= 0; j--) {
// 交换大根堆顶部和数组末尾,数据末尾元素来到最终位置
swap(arr, 0, j);
// 依次对父节点为0位置,数组长度为i的元素进行堆化操作
heapify(arr, 0, j);
}
}
// 建立大根堆
private static void buildMaxHeap(int[] arr) {
for (int i = (arr.length - 2) / 2; i >= 0; i--) {
heapify(arr, i, arr.length);
}
}
// 对 arr[0, n) 所形成的最大堆中,索引为parent的父节点元素,进行heapify操作
private static void heapify(int[] arr, int parent, int n) {
while (2 * parent + 1 < n) {
int left = 2 * parent + 1;
if (left + 1 < n && arr[left] < arr[left + 1]) {
left++;
}
// 大根堆,父最大,就停止遍历
if (arr[left] <= arr[parent]) {
break;
}
// 否则就最大值交换到父节点
swap(arr, parent, left);
// 父节点循环进行下去
parent = left;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
} LC912 排序数组
排序算法是否掌握,可以用力扣912验证
class Solution {
// 排序算法是否掌握,用力扣912检验
public int[] sortArray(int[] arr) {
if (arr.length < 2) {
return arr;
}
for (int i = (arr.length - 2) / 2; i >= 0; i--) {
heapify(arr, i, arr.length);
}
for (int j = arr.length - 1; j >= 0; j--) {
swap(arr, 0, j);
heapify(arr, 0, j);
}
return arr;
}
private static void heapify(int[] arr, int parent, int n) {
while (2 * parent + 1 < n) {
int left = 2 * parent + 1;
if (left + 1 < n && arr[left] < arr[left + 1]) {
left++;
}
// 大根堆,父最大,就停止遍历
if (arr[left] <= arr[parent]) {
break;
}
swap(arr, parent, left);
parent = left;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
} LC215 找出第K个最大元素
题目:在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
备注:高频面试题,解法很多,但面试官最希望的就是手写堆排序
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 输出: 4
public class Solution {
// 方法1:手写大根堆
public int findKthLargest1(int[] nums, int k) {
buildMaxHeap(nums);
// 数组首位交换k次,数组首位就是第K大的数
for (int i = nums.length - 1; i > nums.length - k; i--) {
swap(nums, 0, i);
heapify(nums, 0, i);
}
return nums[0];
}
// 建立大根堆
private static void buildMaxHeap(int[] arr) {
for (int i = (arr.length - 2) / 2; i >= 0; i--) {
heapify(arr, i, arr.length);
}
}
// 对 arr[0, n) 所形成的最大堆中,索引为parent的父节点元素,进行heapify操作
private static void heapify(int[] arr, int parent, int n) {
while (2 * parent + 1 < n) {
int left = 2 * parent + 1;
if (left + 1 < n && arr[left] < arr[left + 1]) {
left++;
}
// 大根堆,父最大,就停止遍历
if (arr[left] <= arr[parent]) {
break;
}
// 否则就最大值交换到父节点
swap(arr, parent, left);
// 父节点循环进行下去
parent = left;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 方法2:使用PriorityQueue大根堆
public int findKthLargest2(int[] nums, int k) {
// 优先级队列默认是小根堆,拉姆达表达式改成大根堆
Queue<Integer> maxQueue = new PriorityQueue<>((a, b) -> b - a);
for (int num : nums) {
maxQueue.add(num);
}
// 第1大的数,第2大的数,第K大的数
// 移0次 移1次 移K-1次
for (int i = 0; i < k - 1; i++) {
maxQueue.remove();
}
return maxQueue.peek();
}
// 方法2:PriorityQueue小根堆
public int findKthLargest3(int[] nums, int k) {
// 因为第k大元素是k个元素中最小的,所以用小根堆
Queue<Integer> minQueue = new PriorityQueue<>();
// 先将数组前k个数放入小根堆
for (int i = 0; i < k; i++) {
minQueue.add(nums[i]);
}
// [1,2,3,4]中第2大的数是3,[1,2]的小根堆顶是2,比下一个数3小所以出队
// 因为排序后的后k个数虽然是这k个数中最小的,但确实非k个数中最大的,所以小根堆堆顶小了就要出队再入当前元素
for (int i = k; i < nums.length; i++) {
if (!minQueue.isEmpty() && minQueue.peek() < nums[i]) {
minQueue.remove();
minQueue.add(nums[i]);
}
}
// 此时小根堆堆顶,就是第K大的元素
return minQueue.peek();
}
// 法4:选择排序法
public int findKthLargest4(int[] nums, int k) {
// 外层循环k次
for (int i = 0; i < k; i++) {
// 找最大值就行
int maxIndex = i;
for (int j = i + 1; j < nums.length; j++) {
maxIndex = nums[maxIndex] < nums[j] ? j : maxIndex;
}
swap(nums, i, maxIndex);
}
// 第一次最大值下标=1-1=0;
// 第K次最大值下标=k-1
return nums[k - 1];
}
} 剑指Offer40 最小的第K个数
public class Solution {
// 最小的第K个数,用大根堆
public int[] getLeastNumbers(int[] arr, int k) {
// 优先级队列默认是小根堆,添加参数修改成大根堆
Queue<Integer> maxQueue = new PriorityQueue<>(11, (o1, o2) -> o2 - o1);
// 原数组前k个数放入大根堆
for (int i = 0; i < k; i++) {
maxQueue.add(arr[i]);
}
for (int i = k; i < arr.length; i++) {
// 大根堆堆顶比大,就移除
if (!maxQueue.isEmpty() && maxQueue.peek() > arr[i]) {
maxQueue.remove();
maxQueue.add(arr[i]);
}
}
// 依次移出大根堆,返回结果集数组中
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = maxQueue.remove();
}
return res;
}
} 快速排序
单路快排
public class QuickSort {
public static void quickSort(int[] arr) {
// random作为参数一致传递下去
Random random = new Random();
quickSort(arr, 0, arr.length - 1, random);
}
private static void quickSort(int[] arr, int l, int r, Random random) {
// 递归结束条件:显然只剩一个数时,递归结束
if (l >= r) {
return;
}
// 先划分区间
int p = partition(arr, 0, arr.length - 1, random);
// 左边递归排序
quickSort(arr, l, p - 1, random);
// 右边递归排序
quickSort(arr, p + 1, r, random);
}
private static int partition(int[] arr, int l, int r, Random random) {
int randomIndex = l + random.nextInt(r - l + 1);
swap(arr, l, randomIndex);
// j初始化为左边界
int j = l;
// 从左边界第一个数开始遍历
for (int i = l + 1; i < arr.length; i++) {
// j此时要求是指向<区域最后一个数
if (arr[i] < arr[l]) {
// 发现<arr[l],先移动j+1使得j指向>=arr[j]的第一个数,然后交换[i,j]位置的数
swap(arr, i, j++);
}
}
// 由于循环跳出j指向<区域最后一个数,与基准arr[l]交换
swap(arr, l, j);
// 交换后,j指向>=第一个数,返回该数=下标值
return j;
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
} 双路快排
public class QuickSort {
// 默写版
public static void quickSort(int[] arr) {
// random作为参数一致传递下去
Random random = new Random();
quickSort(arr, 0, arr.length - 1, random);
}
private static void quickSort(int[] arr, int l, int r, Random random) {
// 递归结束条件:显然只剩一个数时,递归结束
if (l >= r) {
return;
}
// 先划分区间
int p = partition(arr, l, r, random);
// 左边递归排序
quickSort(arr, l, p - 1, random);
// 右边递归排序
quickSort(arr, p + 1, r, random);
}
private static int partition(int[] arr, int l, int r, Random random) {
int randomIndex = l + random.nextInt(r - l + 1);
swap(arr, l, randomIndex);
// j初始化为左边界
int j = l;
// 从左边界第一个数开始遍历
for (int i = l + 1; i < arr.length; i++) {
// j此时要求是指向<区域最后一个数
if (arr[i] < arr[l]) {
// 发现<arr[l],先移动j+1使得j指向>=arr[j]的第一个数,然后交换[i,j]位置的数
swap(arr, i, j++);
}
}
// 由于循环跳出j指向<区域最后一个数,与基准arr[l]交换
swap(arr, l, j);
// 交换后,j指向>=第一个数,返回该数=下标值
return j;
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
} 三路快排
public class QuickSort2 {
public static void quickSort3way(int[] arr) {
if (arr.length < 2) {
return;
}
Random random = new Random();
quickSort3way(arr, 0, arr.length - 1, random);
}
private static void quickSort3way(int[] arr, int l, int r, Random random) {
if (l >= r) {
return;
}
// 分治:因为三路快排中间不用递归,避免partition返回值麻烦,所以partition和quickSort3ways写在一起
int randomIndex = l + random.nextInt(r - l + 1);
swap(arr, l, randomIndex);
// 核心:arr[l+1,lt]<v ; arr[lt+1,i-1]=v ; arr[gt,r]>v
// lt指向<的最后一个元素,i指针遍历,gt指向>的第一个元素
int lt = l, i = l + 1, gt = r + 1;
while (i < gt) {
if (arr[i] < arr[l]) {
lt++;
swap(arr, i, lt);
i++;
} else if (arr[i] > arr[l]) {
gt--;
swap(arr, i, gt);
// i所在位置从后往前发生了交换,导致nums[i]变新,需要重新比较,i无需++
} else {
i++;
}
}
// 交换l位置和小于第一个数
swap(arr, l, lt);
// 三路快排抛弃掉中间的部分,不再递归
quickSort3way(arr, l, lt - 1, random);
quickSort3way(arr, gt, r, random);
}
private static void swap(int[] arr, int i, int j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
} LC169 最多的数字
题目:给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入:[3,2,3] 输出:3
示例 2:
输入:[2,2,1,1,1,2,2] 输出:2
public class Solution {
// 这道题可以用排序法,但是都没有摩尔投票法快
public int majorityElement(int[] nums) {
int cur = 0, count = 0;
for (int num : nums) {
// count为0,说明要更新遍历数字
if (count == 0) {
cur = num;
}
// 相同的数组就+1
if (cur == num) {
count++;
} else {
count--;
}
}
return cur;
}
} 归并排序
public class MergeSort {
public static void mergeSort(int[] arr) {
// 临时数组一开始就创建,传递到merge将arr复制给temp数组
int[] temp = Arrays.copyOf(arr, arr.length);
mergeSort(arr, 0, arr.length - 1, temp);
}
private static void mergeSort(int[] arr, int l, int r, int[] temp) {
if (l >= r) {
return;
}
// 先递归,划分区域
int mid = l + (r - l) / 2;
mergeSort(arr, l, mid, temp);
mergeSort(arr, mid + 1, r, temp);
// 再归并,mid>前一个数才归并
if (arr[mid] > arr[mid + 1]) {
merge(arr, l, mid, r, temp);
}
}
private static void merge(int[] arr, int l, int mid, int r, int[] temp) {
System.arraycopy(arr, l, temp, l, r - l + 1);
int p = l, q = mid + 1;
for (int i = l; i <= r; i++) {
if (p > mid) {
arr[i] = temp[q++];
} else if (q > r) {
arr[i] = temp[p++];
} else if (temp[p] <= temp[q]) {
arr[i] = temp[p++];
} else {
arr[i] = temp[q++];
}
}
}
} LC88 合并两个有序数组
题目:给你两个有序整数数组 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]
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0 输出:[1]
public class Solution {
// 面试最高频的一道题
// 方法1:双指针,从前往后遍历两个数组,需要辅助数组
public static void merge1(int[] nums1, int m, int[] nums2, int n) {
// temp存储每次排序好的数组
int[] temp = new int[m + n];
// p遍历nums1,q遍历nums2,i遍历temp
int p = 0, q = 0, i = 0;
while (p < m && q < n) {
temp[i++] = nums1[p] < nums2[q] ? nums1[p++] : nums2[q++];
}
while (p < m) {
temp[i++] = nums1[p++];
}
while (q < n) {
temp[i++] = nums2[q++];
}
// 将temp数组遍历回nums1
System.arraycopy(temp, 0, nums1, 0, m + n);
}
// 方法2:双指针,从后往前遍历两个数组,放最大,不需要辅助数组
public static void merge2(int[] nums1, int m, int[] nums2, int n) {
int p = m - 1, q = n - 1, i = m + n - 1;
// 从后往前找大的放入nums1中
while (p >= 0 && q >= 0) {
nums1[i--] = nums1[p] < nums2[q] ? nums2[q--] : nums1[p--];
}
// p<0上面遍历结束,此时q还没遍历到0,将nums2从[0,q+1)复制到nums1中
System.arraycopy(nums2, 0, nums1, 0, q + 1);
}
}
LC21 合并两个有序链表
题目:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = [] 输出:[]
示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
递归的解法,常看动图,加深理解:力扣21递归解法查看
public class Solution {
// 迭代法:preHead.next返回结果,pre用于调整较小值
public ListNode mergeTwoLists1(ListNode l1, ListNode l2) {
// 设定一个哑结点,方便返回值
ListNode dummyNode = new ListNode(-1);
// cur指针指向每次比较的较小值结点
ListNode cur = dummyNode;
while (l1 != null && l2 != null) {
// 判断较小值,cur指向它
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
// 判断完后移cur
cur = cur.next;
}
// 循环结束,cur指向非空链表头部
cur.next = (l1 == null) ? l2 : l1;
return dummyNode.next;
}
// 递归法:判断 l1 和 l2 头结点哪个更小,然后较小结点的 next 指针指向其余结点的合并结果
// 因为l1、l2有序,所以递归最后一定是指向一个最短有序链表的,然后递归依次返回
public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
// 递归结束条件:谁遍历到最后为null,返回另一个
if (l1 == null || l2 == null) {
return l1 == null ? l2 : l1;
}
// 递归判断,每一次都返回最小值结点进递归栈
if (l1.val < l2.val) {
l1.next = mergeTwoLists2(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists2(l1, l2.next);
return l2;
}
}
} 剑指51 数组中的逆序对问题
题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4] 输出: 5
public class Solution {
// 将res作为参数传递,会出现各种问题,直接定义成员变量省事
private int res;
// 归并排序法,原理是利用nums[i]>nums[j],那么[i,mid]中都是逆序对个数
public int reversePairs(int[] nums) {
int[] temp = new int[nums.length];
res = 0;
mergeSort(nums, 0, nums.length - 1, temp);
return res;
}
private void mergeSort(int[] nums, int l, int r, int[] temp) {
if (l >= r) {
return;
}
int mid = l + (r - l) / 2;
mergeSort(nums, l, mid, temp);
mergeSort(nums, mid + 1, r, temp);
if (nums[mid] > nums[mid + 1]) {
merge(nums, l, mid, r, temp);
}
}
private void merge(int[] nums, int l, int mid, int r, int[] temp) {
System.arraycopy(nums, l, temp, l, r - l + 1);
int p = l, q = mid + 1;
for (int i = l; i <= r; i++) {
if (p > mid) {
nums[i] = temp[q++];
} else if (q > r) {
nums[i] = temp[p++];
} else if (temp[p] <= temp[q]) {
// <=区域不会形成逆序对,所以和归并排序过程一样
nums[i] = temp[p++];
} else {
// >说明必构成逆序对:[p,mid]与[mid+1,...]构成逆序对mid-p+1
res += mid - p + 1;
nums[i] = temp[q++];
}
}
}
} 非比较算法O(n)
计数排序
- 计数排序不基于元素的比较,只基于元素的出现次数,用R来表示一个元素的取值范围,利用count[R]记录每个元素出现次数,index[R+1]记录nums排好序后[index[i],index[i+1])的值为i
- 计数排序是稳定的,这是它最关键的一个性质
力扣75 颜色分类
题目:给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
示例 1:
输入:nums = [2,0,2,1,1,0] 输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1] 输出:[0,1,2]
示例 3:
输入:nums = [0] 输出:[0]
示例 4:
输入:nums = [1] 输出:[1]
public class Solution {
// 方法1:简单的记录出现元素次数,但对于理解计数排序法非常有帮助
public void sortColors1(int[] nums) {
// count数组记录0,1,2在nums中出现的次数
int[] count = new int[3];
for (int num : nums) {
if (num == 0) {
count[0]++;
} else if (num == 1) {
count[1]++;
} else {
count[2]++;
}
// 以上的写法可以等价:count[num]++;
}
// 注意下标之间的连接
for (int i = 0; i < count[0]; i++) {
nums[i] = 0;
}
for (int i = count[0]; i < count[1] + count[0]; i++) {
nums[i] = 1;
}
for (int i = count[1] + count[0]; i < count[2] + count[1] + count[0]; i++) {
nums[i] = 2;
}
}
// 方法2:计数排序法,这个方法有点像桶排序
public void sortColors2(int[] nums) {
// 1.定义元素取值范围:[0,3)
int R = 3;
int[] cnt = new int[R];
// 记录出现的次数
for (int num : nums) {
cnt[num]++;
}
// 2.index表示:nums排好序后[index[i],index[i+1])的值为i
// 因为0之前没有元素,单独用0位置存,所以是R+1个长度
int[] index = new int[R + 1];
// 只遍历到n-1的位置,因为末尾元素无cnt[R+1]对应
for (int i = 0; i < index.length - 1; i++) {
index[i + 1] = index[i] + cnt[i];
}
// 3.再次遍历到n-1,nums[index[i],index[i+1])=i
for (int i = 0; i < index.length - 1; i++) {
for (int j = index[i]; j < index[i + 1]; j++) {
nums[j] = i;
}
}
}
} 力扣1122 数组的相对顺序
题目:给你两个数组,arr1和 arr2,
- arr2 中的元素各不相同
- arr2 中的每个元素都出现在 arr1 中
- 对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。
示例:
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6] 输出:[2,2,2,1,4,3,3,9,6,7,19]
提示:数组长度不大于1k,每个元素取值返回不大于1k,就是提示你用计数排序法
1 <= arr1.length, arr2.length <= 1000 0 <= arr1[i], arr2[i] <= 1000 arr2 中的元素 arr2[i] 各不相同 arr2 中的每个元素 arr2[i] 都出现在 arr1 中
public class Solution {
// 计数排序法,取值范围为[0,1000]所以可以考虑使用计数排序法
public int[] relativeSortArray(int[] arr1, int[] arr2) {
// 1.找到arr1中最大值max,生成count[max+1],count[0,max]记录arr1每个元素出现次数
int max = 0;
for (int num : arr1) {
max = Math.max(num, max);
}
// 2.只用计数排序中的count数组记录次数就行,不需要index数组
int[] count = new int[max + 1];
for (int num : arr1) {
count[num]++;
}
int[] res = new int[arr1.length];
int index = 0;
// 3.遍历arr2中元素,回count[num]=出现次数,放回res中
for (int num : arr2) {
for (int i = 0; i < count[num]; i++) {
res[index++] = num;
}
// 重点:用过的count[num]清零,说明
count[num] = 0;
}
// 4.遍历count,非0元素遍历个数回res中
for (int i = 0; i < count.length; i++) {
if (count[i] != 0) {
for (int j = 0; j < count[i]; j++) {
res[index++] = i;
}
}
}
return res;
}
} 基数排序
什么是数字的高位和低位?比如102,高位是1,低位是2,不要弄反
LSD算法
- 适用于等长字符串,通常不会用于数字的比较
- 如果用于比较数字,可以将数字转换为字符串数组,不等长的数字,前面高位补0再转换,并不会改变原排序位置
- 但是LSD不能用于非等长字符串,字符串补空格是行不通的,\0也是不行(某些语言\0是字符串结尾标识)
- 如果要比较非等长字符串,那就要用MSD算法
public class LSDSort {
// 参数提供一个len,提示调用者arr中元素等长的长度
public static void lSDSort(String[] arr, int len) {
// LSD算法,多用于字符串排序,这里参数直接使用字符串数组;并且LSD只适用于等长字符串
// LSD:Least significant digital,比如"ABC"中的“C”是低位,“A”是高位
for (String s : arr) {
if (s.length() != len) {
throw new IllegalArgumentException("All Strings' length must be the same.");
}
}
// Java中Char占1个字节=256位,取值范围0-255
int R = 256;
int[] cnt = new int[R];
int[] index = new int[R + 1];
// 辅助数组
String[] temp = new String[arr.length];
// LSD:从每个字符的末尾开始使用计数排序
for (int i = len - 1; i >= 0; i--) {
// 每次遍历开始,记录次数前清空cnt
Arrays.fill(cnt, 0);
// 记录次数
for (String s : arr) {
cnt[s.charAt(i)]++;
}
// 记录index数组
for (int j = 0; j < index.length - 1; j++) {
index[j + 1] = index[j] + cnt[j];
}
// 根据index,将各字符放回temp数组
for (String s : arr) {
// index[s.charAt(i)]=是s起始出现的坐标
temp[index[s.charAt(i)]] = s;
// 下一次出现的坐标+1
index[s.charAt(i)]++;
}
// 遍历temp赋值给原数组arr
for (int j = 0; j < arr.length; j++) {
arr[j] = temp[j];
}
}
}
public static void main(String[] args) {
String[] arr = {"a", "c", "d"};
LSDSort.lSDSort(arr, 1);
System.out.println(Arrays.toString(arr));
}
} MSD算法
- 边界处理:空(长度不够)对应的“字符值”最小,比如空比“A”还小
- 假设原先字符取值范围[0,R-1]R种可能,现在加上空值的可能,字符取值范围为[1,R]+[0],规定[0]是空,长度为R+1
public class MSDSort {
public static void mSDSort(String[] arr) {
int N = arr.length;
String[] temp = new String[N];
mSDSort(arr, 0, N - 1, 0, temp);
}
/**
* @param arr 待排序数组arr
* @param left 对arr[i]待排序的最左边字符串
* @param right 对arr[i]待排序的最右边字符串
* @param r arr[i]的r位置,处理arr[left, right]
* @param temp 辅助数组一次性开辟出来
*/
private static void mSDSort(String[] arr, int left, int right, int r, String[] temp) {
// 递归结束条件
if (left >= right) {
return;
}
// 开始修改计数排序
// 字符取值范围
int R = 256;
// 由于有null值,index和cnt数组比LSD都要加一位
// 规定cnt[0]记录空值出现的次数
int[] cnt = new int[R + 1];
int[] index = new int[R + 2];
// 记录arr[i]中r位置字符出现的次数
for (int i = left; i <= right; i++) {
// 越界:r位置如果超过了字符的长度=遇到了空值,规定了cnt[0]记录空值,所以cnt[0]++
// 正常:cnt[arr[i].charAt(r) + 1]++
cnt[r >= arr[i].length() ? 0 : (arr[i].charAt(r) + 1)]++;
}
// 记录次数
for (int i = 0; i < cnt.length; i++) {
index[i + 1] = index[i] + cnt[i];
}
// 赋值给temp数组
for (int i = left; i <= right; i++) {
int p = r >= arr[i].length() ? 0 : (arr[i].charAt(r) + 1);
temp[left + index[p]] = arr[i];
index[p]++;
}
// temp赋值回原数组
for (int i = left; i <= right; i++) {
arr[i] = temp[i];
}
// 计数排序修改完成,开始递归,需要深刻理解
for (int i = 0; i < R; i++) {
mSDSort(arr, left + index[i], left + index[i + 1] - 1, r + 1, temp);
}
}
} 桶排序
public class BucketSort {
// 桶排序1:桶记录每个元素出现的次数
public static void bucketSort1(Integer[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 1.从所有数中找出最大值
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
// 2.生成max+1个元素的桶,使得桶[0,max]
int[] bucket = new int[max + 1];
// 3.遍历数组,每个元素出现的个数加入桶
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]]++;
}
// 4.遍历桶,将非0数据放回原数组
int index = 0;
for (int i = 0; i < bucket.length; i++) {
while (bucket[i]-- > 0) {
arr[index++] = i;
}
}
}
/**
* 桶排序2:用户指定每个桶的长度,另一种思维
*
* @param arr 待排序数组
* @param c 每个桶的长度
*/
public static void bucketSort2(int[] arr, int c) {
if (c <= 0) {
throw new IllegalArgumentException("c must be >0");
}
// 计算桶的数量
int B = arr.length / c + (arr.length % c > 0 ? 1 : 0);
List<Integer>[] buckets = new List[B];
for (int i = 0; i < B; i++) {
buckets[i] = new LinkedList<>();
}
// 找出数组中的最小值
int min = Integer.MAX_VALUE;
for (int num : arr) {
min = Math.min(min, num);
}
// 元素如桶:在几号桶? = (arr[i]-min)/桶数c
for (int num : arr) {
buckets[(num - min) / c].add(num);
}
// 使用库函数对每个桶中元素进行排序
for (int i = 0; i < buckets.length; i++) {
Collections.sort(buckets[i]);
}
// 将桶中元素,放回原数组
int index = 0;
for (int i = 0; i < buckets.length; i++) {
for (Integer num : buckets[i]) {
arr[index++] = num;
}
}
}
}
力扣164 最大间距
给定一个无序的数组,找出数组在排序之后,相邻元素之间最大的差值。
如果数组元素个数小于 2,则返回 0。
示例 1:
输入: [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
示例 2:
输入: [10] 输出: 0 解释: 数组元素个数小于 2,因此返回 0。
public class Solution {
public int maximumGap(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
// 1.统计数组长度,最大值,最小值
int N = nums.length;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i = 0; i < N; i++) {
min = Math.min(min, nums[i]);
max = Math.max(max, nums[i]);
}
// 如果最大值和最小值相同,最大间距为0,直接返回
if (min == max) {
return 0;
}
// 2.生成3个桶,每个桶长N+1
boolean[] hasNum = new boolean[N + 1];
int[] maxs = new int[N + 1];
int[] mins = new int[N + 1];
// 3.最大值桶只放区间最大值,最小值桶只放区间最小值
// bid是桶id,记录放入桶的编号
int bid = 0;
// 有几个数就要遍历几次=遍历N次
for (int i = 0; i < N; i++) {
// 算出每个元素放入桶中的id
bid = getBucketId(nums[i], N, min, max);
mins[bid] = hasNum[bid] ? Math.min(mins[bid], nums[i]) : nums[i];
maxs[bid] = hasNum[bid] ? Math.max(maxs[bid], nums[i]) : nums[i];
hasNum[bid] = true;
}
// 4.排序后,相邻元素的最大差值=(当前非空桶最小值-上一个非空桶最大值)中的最大值
int maxGap = 0;
int lastMax = maxs[0];
// 从第2个桶开始统计最大差值
for (int i = 1; i <= N; i++) {
if (hasNum[i]) {
// 相邻元素的最大差值=(当前非空桶最小值-上一个非空桶最大值)中的最大值
maxGap = Math.max(maxGap, mins[i] - lastMax);
// 更新上一个非空桶的最大值
lastMax = maxs[i];
}
}
return maxGap;
}
// 计算num放入哪个桶:(arr[i]-min)/桶长,桶长=(max-min)/len
// 注意:数据类型全部用long,因为有乘法int会溢出
private int getBucketId(long num, long len, long min, long max) {
return (int) ((num - min) * len / (max - min));
}
} LC561 数组拆分I
题目:给定长度为 2n 的整数数组 nums ,你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从 1 到 n 的 min(ai, bi) 总和最大。
返回该 最大总和 。
示例 1:
输入:nums = [1,4,3,2] 输出:4 解释:所有可能的分法(忽略元素顺序)为: 1. (1, 4), (2, 3) -> min(1, 4) + min(2, 3) = 1 + 2 = 3 2. (1, 3), (2, 4) -> min(1, 3) + min(2, 4) = 1 + 2 = 3 3. (1, 2), (3, 4) -> min(1, 2) + min(3, 4) = 1 + 3 = 4 所以最大总和为 4
示例 2:
输入:nums = [6,2,6,5,1,2] 输出:9 解释:最优的分法为 (2, 1), (2, 5), (6, 6). min(2, 1) + min(2, 5) + min(6, 6) = 1 + 2 + 6 = 9
public class Solution {
// 力扣561:2n长度数组拆分n对(a,b)形式,找出每队最小值之和的最大和
public int arrayPairSum(int[] nums) {
Arrays.sort(nums);
int res = 0;
// 排序后,间隔为2的每位数之和=n对最小值之和的最大和
for (int i = 0; i < nums.length; i += 2) {
res += nums[i];
}
return res;
}
} 排序算法总结
情绪(稳定性)不稳定:快(快速排序)些(希尔排序)选(选择排序)堆(堆排序)
| 排序名称 | 时间复杂度 | 空间复杂度 | 稳定性 | 备注 |
|---|---|---|---|---|
| 冒泡排序 | O(n^2),完全有序变成O(n) | O(1) | 稳定 | 力扣283移动数字0 |
| 选择排序 | O(n^2) | O(1) | 不稳定 | 运行时间和输入无关;数据移动是最少的 |
| 插入排序 | O(n^2),完全有序变成O(n) | O(1) | 稳定 | 排序时间取决于初始值(使用交换方式) |
| 希尔排序 | O(nlogn)-O(n^2) | O(1) | 不稳定 | 分组思想 |
| 堆排序 | O(nlogn) | O(1) | 不稳定 | 实现优先队列 |
| 快速排序 | O(nlogn),含有相同元素数组,三路快排O(n) | O(1) | 不稳定 | 求解topK等问题 |
| 归并排序 | O(nlogn),完全有序变成O(n) | O(n) | 稳定 | 求解逆序对、小和、染色等问题 |

京公网安备 11010502036488号