声明: 1. 本文为我的个人复习总结, 并非那种从零基础开始普及知识 内容详细全面, 言辞官方的文章
2. 由于是个人总结, 所以用最精简的话语来写文章
3. 若有错误不当之处, 请指出
总览:
名称 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 |
---|---|---|---|
冒泡 | O(N^2) | O(N^2) | O(1) |
选择 | O(N^2) | O(N^2) | O(1) |
插入 | O(N^2) | O(N^2) | O(1) |
希尔 | O(N^2) | O(N*logN) | O(1) |
快排 | O(N^2) | O(N*logN) | O(logN) |
归并 | O(N*logN) | O(N*logN) | O(n) |
桶 | O(N+K) | O(N+K) | O(N+K) |
堆 | O(N*logN) | O(N*logN) | O(1) |
不稳定排序: 快选希堆
交换排序: 冒泡 & 快排
比较排序: 其他
bubbleSort03 & 插入排序 都很适合于有序度高的集合
选择排序的交换次数<冒泡排序
1. 冒泡:
public void bubbleSort01(int[] arr) {
int len = arr.length;
for (int i = 0; i < len; i++) { // 循环次数
for (int j = 0; j < len - 1 - i; j++) { // i轮循环后,最后i位就确定已经是最大的了,无需再比较
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
public void bubbleSort02(int[] arr) {
int len = arr.length;
for (int i = 0; i < len; i++) {
boolean flag = true;
for (int j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = false;
}
}
// 优化: 如果一轮比较下来,flag始终为被置为false,说明已经全局有序,可以提前提出
if (flag) {
return;
}
}
}
public void bubbleSort03(int[] arr) {
int len = arr.length;
//优化: recentMaxNumIdx, 上一轮交换的最后一次idx,可以当作下一轮比较目的地的临界值
int recentMaxNumIdx = len - 1;
int last = recentMaxNumIdx; // 随便给个值就行,若经历循环则last必被赋值,否则flag为true就return就结束了
for (int i = 0; i < len; i++) {
boolean flag = true;
for (int j = 0; j < recentMaxNumIdx; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = false;
}
last = j;
}
recentMaxNumIdx = last;
if (flag) {
return;
}
}
}
2. 选择:
public void selectSort(int[] arr) {
int len = arr.length;
// 思路:分为两个区[已排序的区 + 未排序的区],
// 每次从未排序的区里挑选minNum,和未排序分区的首个idx进行swap,这样导致了 已排序的区++,未排序的区--
// 最开始的时候 已排序的区中没数据
// i就是未排序分区的首个idx
// 因为 j(即i+1) < len, 所以i < len - 1;
// 或者这样理解: i是len-1时,只有最后一个数字没进行排序,其他都从最小数字开始按升序排好了,
// 言外之意此数字就是最大的了,没必要再进行排序
for (int i = 0; i < len - 1; i++) {
int minNumIdx = i;
for (int j = i + 1; j < len; j++) {
if (arr[j] < arr[minNumIdx]) {
minNumIdx = j;
}
}
int temp = arr[i];
arr[i] = arr[minNumIdx];
arr[minNumIdx] = temp;
}
}
3. 插入
public void insertSort(int[] arr) {
int len = arr.length;
// 思路: 分为两个区[已排序的区 + 未排序的区],
// 每次从未排序的区里挑选首个数字,然后往前遍历,在已排序分区中寻求一个位置,然后导致了 已排序的区++,未排序的区--
// 最开始的时候 已排序的区中只有arr[0]
// i是没排序分区中的首个元素idx
for (int i = 1; i < len; i++) {
int willInsertIdx = i;
int willInsertNum = arr[i]; // 待插入的元素值
while (willInsertIdx >= 1) {
if (arr[willInsertIdx - 1] > willInsertNum) {
arr[willInsertIdx] = arr[willInsertIdx - 1];
willInsertIdx--;
} else {
break;
}
}
arr[willInsertIdx] = willInsertNum;
}
}
4. 希尔
// 插入排序在逆序度高时,移动元素次数太多
// 插入排序相当于gap间隙为一的希尔排序
// 希尔排序先gap=1,再=2,再=1
// 这样在即使逆序度高时,也不必移动元素太多次
public void shellSort(int[] arr) {
int len = arr.length;
for (int gap = len / 2; gap >= 1; gap /= 2) {
for (int i = gap; i < len; i++) {
int willInsertIdx = i;
int willInsertNum = arr[i];
while (willInsertIdx >= gap) {
if (arr[willInsertIdx - gap] > willInsertNum) {
arr[willInsertIdx] = arr[willInsertIdx - gap];
willInsertIdx -= gap;
} else {
break;
}
}
arr[willInsertIdx] = willInsertNum;
}
}
}
5. 快排
public void quickSort(int[] arr, int lp, int rp) {
if (lp >= rp) {
return;
}
int base = partition(arr, lp, rp);
quickSort(arr, lp, base - 1);
quickSort(arr, base + 1, rp);
}
// 1,5,9,2,7,3
// 1,5,9,2,7,3
// 1,2,9,5,7,3
// 1,2,3,5,7,9
// 基准元素应该放到哪个idx,并且把小于基准元素的放在idx左边,剩下右边的就是大于基准元素的了
private int partition(int[] arr, int lp, int rp) {
// baseNum随便选的,这里统一选成最右边的元素值
int baseNum = arr[rp];
int baseIdx = lp;
for (int i = lp; i < rp; i++) {
if (arr[i] < baseNum) {
swap(arr, i, baseIdx);
baseIdx++;
}
}
swap(arr, rp, baseIdx);
return baseIdx;
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
6. 归并
数组时 递归时需要传递temp[], 使其都是同一个数组; 链表时, 因为可以传递引用, 便不用传参temp[]
数组:
// 分+合
public static void mergeSort(int[] arr, int lp, int rp, int[] temp) {
if (lp < rp) {
int mid = (lp + rp) / 2;
// 向左递归分解
mergeSort(arr, lp, mid, temp);
// 向右递归分解
mergeSort(arr, mid + 1, rp, temp);
// 合并
merge(arr, lp, mid, rp, temp);
}
}
// 合并 两个升序数组
// arr[lp~mid]是一个升序数组,arr[mid+1~rp]是一个升序数组
public static void merge(int[] arr, int lp, int mid, int rp, int[] temp) {
int lp1 = lp; // 左边有序序列的初始索引
int rp1 = mid;
int lp2 = mid + 1; // 右边有序序列的初始索引
int rp2 = rp;
int cnt = 0;
while (lp1 <= rp1 && lp2 <= rp2) {
if (arr[lp1] <= arr[lp2]) {
temp[cnt++] = arr[lp1++];
} else {
temp[cnt++] = arr[lp2++];
}
}
// 把有剩余数据的数组余留元素 填充到temp
while (lp1 <= mid) {
temp[cnt++] = arr[lp1++];
}
while (lp2 <= rp) {
temp[cnt++] = arr[lp2++];
}
// 拷贝temp数组到arr
System.arraycopy(temp, 0, arr, lp, rp - lp + 1);
}
链表:
public ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode fast = head, slow = head;
ListNode pre = null;
// 循环至少会发生一次,因为上文 if(head == null || head.next == null)就return了
while (fast != null && fast.next != null) {
fast = fast.next.next;
pre = slow;
slow = slow.next;
}
// 找mid结点
ListNode mid = slow;
// 截断
pre.next = null;
// 递归拆分
ListNode left = mergeSort(head);
ListNode right = mergeSort(mid);
ListNode dummy = new ListNode(0);
ListNode temp = dummy;
// 合并
while (left != null && right != null) {
if (left.val < right.val) {
temp.next = left;
left = left.next;
} else {
temp.next = right;
right = right.next;
}
temp = temp.next;
}
// 合并有剩余的单个链表
temp.next = left != null ? left : right;
return dummy.next;
}
7. 堆
大顶堆是对数组进行升序排序
/*
堆排序 = 构造大顶堆(需要遍历所有非叶子结点) + 维护大顶堆(只需调整根一个结点) + 选择排序
规律: i从0开始,arr[i]的左孩子是2*i+1,右孩子是2*i+2
*/
// 堆排序
private void heapSort(int[] arr) {
int len = arr.length;
// 构造大顶堆
buildMaxHeap(arr);
// 选择排序+维护大顶堆
// 末尾元素部分,就是已经有序的部分
// 逐步将每个最大值的根节点与末尾元素交换,并且再调整二叉树,使其成为大顶堆
for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i); // swap 根 和 当前未排序部分的最后一个结点
heapAdjust(arr, 0, i); // 调整根结点使其能符合大顶堆,i后面的都已经排好序了
}
}
// 构造大顶堆
private void buildMaxHeap(int[] arr) {
int len = arr.length;
// 遍历所有的非叶子结点,自底向下调整
for (int i = len / 2; i >= 0; i--) {
heapAdjust(arr, i, len);
}
}
// 维护大顶堆
// 把某个结点 放到合适的位置
// 要调整的结点不仅与左孩子右孩子比较,还要在"下沉"后继续与孙子比较
private void heapAdjust(int[] arr, int targetIdx, int len) {
int maxChildIdx = -1;
int i;
// "下沉"继续比较
for (i = targetIdx; 2 * i + 1 < len; i = maxChildIdx) {
maxChildIdx = 2 * i + 1;
// 如果左子树小于右子树,则需要比较右子树和父节点; 即父亲与最大的孩子进行比较
if (maxChildIdx + 1 < len && arr[maxChildIdx] < arr[maxChildIdx + 1]) {
maxChildIdx++;
}
// 如果父节点小于孩子结点,则需要交换
if (arr[i] < arr[maxChildIdx]) {
swap(arr, i, maxChildIdx);
} else {
break; // 大顶堆结构未被破坏,不需要调整
}
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}