数组排序小结 (对应:NC140 排序 LC912 排序数组)
参考资料:算法第4版,内容包括选择排序、插入排序、归并排序、快速排序和堆排序
[toc]
1. 初级排序算法
1.1 选择排序
遍历数组 (0~n),找出最小元素,将最小元素和索引为0的元素交换;
遍历剩下的数组 (1~n),找出最小元素,将最小元素和索引为1的元素交换;
重复上述过程,直到将整个数组排序完成。
特点:
运行时间和输入无关,即长度相等的有序数组和无序数组所需要的排序时间是一致的。
数据移动最少,即只需要n次交换。
时间复杂度
空间复杂度
1.2 插入排序
从索引1开始遍历数组,将当前的元素和左边的所有元素比较并放入合适的位置,直到索引到达最右端。
public static void insertSort(int[] arr) { for (int i = 1; i < arr.length; i++) { for (int j = i; j > 0 && arr[j] < arr[j-1]; j--) { int temp = arr[j-1]; arr[j-1] = arr[j]; arr[j] = temp; } } }
插入排序所需要的时间取决于输入元素的初始顺序,数组越有序,插入排序越快。
当数组中的倒置(数组中两个顺序颠倒的元素)的数量很少时,插入排序甚至可能比其他所有的排序算法都要快。
时间复杂度 最好情况下(如有序数组) 最坏情况下(如倒序数组)
空间复杂度
2. 归并排序
将两个有序的数组归并成一个更大的有序数组。
可以自顶向下归并、自底向上归并。
原地归并两个已经排序好的数组的过程:
目的:将已排序好的 arr[lo ... mid]
和已排序好的 arr[mid+1 ... hi]
归并
将原数组 arr[lo ... hi]
复制到辅助数组 aux[lo ... hi]
中,两个指针分别从位置lo 和mid+1 开始往后走,比较两者的值,将较小的值依次放入arr[] 中
public static void merge(int[] arr, int lo, int mid, int hi) { int i = lo, j = mid+1; int[] aux = new int[arr.length]; for (int k = lo; k <= hi; k++) { aux[k] = arr[k]; } for (int k = lo; k <= hi; k++) { if (i > mid) { arr[k] = aux[j++]; } else if (j > hi) { arr[k] = aux[i++]; } else if (aux[j] < aux[i]) { arr[k] = aux[j++]; } else { arr[k] = aux[i++]; } } }
自顶向下实现归并排序
private static void mergeSort(int[] arr, int lo, int hi) { if (lo >= hi) { return; } int mid = (hi - lo) / 2 + lo; mergeSort(arr, lo, mid); mergeSort(arr, mid+1, hi); merge(arr, lo, mid, hi); }
自底向上实现归并排序
private static void mergeFromBottom(int[] arr) { int N = arr.length; int[] aux = new int[N]; for (int sz = 1; sz < N; sz = sz+sz) { for (int lo = 0; lo < N - sz; lo += sz+sz) { merge(arr, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1)); } } }
时间复杂度
空间复杂度 即需要使用额外空间存储辅助数组
注:归并排序的代码在leetcode中可以通过,牛客网超时。
时间优化:
- 使用插入排序处理小规模的子数组(比如长度小于15)可以缩短运行时间,一般可节省10%~15%时间。
- 添加判断条件,如果arr[mid] <= arr[mid+1] 则说明两个数组不需要再归并了,可以跳过merge方法。
3. 快速排序
快速排序是一种分治的排序算法。它将找到一个元素把数组分成两个子数组,左边的所有元素不大于这个元素,右边的所有元素不小于这个元素,将两部分独立地排序。当两个子数组都有序时,整个数组也就自然有序了。
切分数组的策略:随意选定arr[lo]作为切分元素,从数组两端开始扫描数组,交换所有不满足“左侧元素不大于arr[lo]”和“右侧元素不小于arr[lo]”条件的元素。当两个指针相遇时,只需要将arr[lo]和左子数组最右侧元素arr[j] 交换之后返回 j 即可。
private static void quickSort(int[] arr, int lo, int hi) { if (hi <= lo) { return; } int j = partition(arr, lo, hi); quickSort(arr, lo, j-1); quickSort(arr, j+1, hi); } private static int partition(int[] arr, int lo, int hi) { int i = lo, j = hi+1; int v = arr[lo]; while (true) { while (arr[++i] < v) { if (i == hi) { break; } } while (v < arr[--j]) { if (j == lo) { break; } } if (i >= j) { break; } exchange(arr, i, j); } exchange(arr, lo, j); return j; }
4. 堆排序
二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆每个节点的左子树和右子树都是一个二叉堆。
当父节点的键值总是大于或等于任何一个子节点的键值时为“最大堆”。当父节点的键值总是小于或等于任何一个子节点的键值时为“最小堆”。
二叉堆排序的思路:遍历未排序的数组,将数组中的每个数放入二叉堆中,再将堆顶元素弹出直至为空。
向二叉堆中添加元素:可将新的元素添加至最后一个叶子节点旁,与其父节点比较,逐渐上浮,即swim() 方法
弹出二叉堆堆顶元素:获得堆顶元素后,可将二叉堆最后一个叶子节点替换掉堆顶元素,将新的堆顶元素下沉至合适的位置,即sink() 方法。
用数组表示二叉堆:由于二叉堆是一个完全二叉树,因此可以采用层序遍历的方式将二叉堆存储在数组中,其中arr[0] 是堆顶元素,对于任意的arr[i],其父节点为arr[(i-1)/2],其左右子节点为 arr[2 * i + 1],arr[2 * i + 2].
二叉堆深度为
时间复杂度
空间复杂度
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * 将给定数组排序 * @param arr int整型一维数组 待排序的数组 * @return int整型一维数组 */ public int[] MySort (int[] arr) { // write code here if (arr == null || arr.length < 2) { return arr; } int[] aux = new int[arr.length]; for (int i = 0; i < arr.length; i++) { aux[i] = arr[i]; swim(aux, i); } for (int i = 0; i < arr.length; i++) { arr[i] = aux[0]; aux[0] = Integer.MAX_VALUE; sink(aux, 0); } return arr; } private void swim(int[] aux, int i) { while (i != 0 && aux[(i-1)/2] > aux[i]) { exch(aux, (i-1)/2, i); i = (i-1)/2; } } private void sink(int[] aux, int i) { while (i * 2 + 1 < aux.length) { int mIndex = i * 2 + 1; if (mIndex + 1 < aux.length && aux[mIndex+1] < aux[mIndex]) { mIndex++; } exch(aux, i, mIndex); i = mIndex; } } private void exch(int[] ns, int p, int q) { int temp = ns[p]; ns[p] = ns[q]; ns[q] = temp; } }