相关概念 稳定 :如果a原本在b前面,而a=b,排序之后a仍然在b的前面。不稳定 :如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
选择排序Selection Sort 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
1 2 3 4 5 6 7 8 9 10 public void selectionSort (int [] nums) { int N = nums.length; for (int i = 0 ; i < N; i++){ int min = i; for (int j = i+1 ; j < N; j++){ if (nums[j] < nums[min]){ min = j; } } swap(nums, i, min); } }
算法分析:不论初始排序如何,都是(N-1)+(N-2)+…+1 ~ $N^2/2$次比较,N次交换 时间复杂度O($n^2$),空间复杂度O(1) 特点:n次交换,不稳定排序((7) 2 5 9 3 4 [7] 1…当我们利用直接选择排序算法进行排序时候,(7)和1调换,(7)就跑到了[7]的后面了,原来的次序改变了) 😁优点:不占用额外的内存空间 😢缺点:时复大 💡应用场景:数据规模较小且对内存空间有限制时
插入排序Insertion Sort 对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1 2 3 4 5 6 7 8 public void insertionSort (int [] nums) { int N = nums.length; for (int i = 1 ; i < N; i++){ for (int j = i; j > 0 && nums[j] < nums[j-1 ]; j--){ swap(nums, j, j-1 ); } } }
算法分析: 最好情况O(n)-初始序列已经是升序: N-1次比较 最坏情况O($n^2$)-初始序列是降序(N-1)+(N-2)+…+1 ~ $N^2/2$次比较,$N^2/2$次交换 平均情况O($n^2$)-每个nums[i]都向前移动一半到nums[i/2] ~ $N^2/4$次比较,$N^2/4$次交换 空间复杂度O(1) 特点:稳定排序(因为插入的过程中都是从后向前进行查找,遇到小于等于(或大于等于)的数停止寻找,进行插入操作。不改变排序前后相等数值的相对顺序) 😁优点:不占用额外的内存空间 😢缺点:时复大 💡应用场景:数据规模较小或者部分数组已排序,且对内存空间有限制时
【优化1】插入排序+二分查找:通过二分查找快速找到插入位置,减少比较次数 比较次数降为O($logn$),交换次数仍为O($n^2$)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void binaryInsertSort (int [] nums) { int N = nums.length; for (int i = 1 ; i < N; i++){ int val = nums[i]; if (nums[i] < nums[i-1 ]){ int idx = binarySearch(nums, i); for (int j = i; j > idx; j--){ nums[j] = nums[j-1 ]; } nums[idx] = val; } } } private int binarySearch (int [] nums, int n) { int l = 0 ; int r = n-1 ; int target = nums[n]; while (l <= r){ int m = l + (r - l) / 2 ; if (nums[m] < target) l = m + 1 ; else if (nums[m] > target) r = m - 1 ; else return m+1 ; } return l; }
【优化2】希尔排序Shell Sort:增加比较间隔-分治思想:对子序列排序→整个序列有序 插入排序的缺点是一次只换一个位置,希尔排序一次可以移动大于1的位置,时复减小。1959年Shell发明,第一个突破O($n^2$)的排序算法。
希尔排序采用分治思想(Divide and Conquer),先将整个序列分割成若干小的子序列,再分别对子序列进行直接插入排序,使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。其核心在于间隔 序列的设定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void shellSort (int [] nums) { int N = nums.length; int dist = 1 ; while (dist < N/3 ){ dist = dist*3 +1 ; } while (dist >= 1 ){ insertionSort(nums, N, dist); dist = dist / 3 ; } } private void insertionSort (int [] nums, int N, int dist) { for (int i = dist; i < N; i++){ for (int j = i; j >= dist && nums[j] < nums[j-dist]; j -= dist){ swap(nums, j, j-dist); } } }
算法分析: 最好情况O(n)-初始序列已经是升序: N-1次比较 最坏情况:O($nlogn$) 平均情况:O($nlogn$) 增量选取:希尔排序的时间复杂度与增量选取有关,但现今没有人能找出希尔排序的精确下界。 特点:不稳定排序(因为在进行分组时,相同元素可能分到不同组中,改变相同元素的相对顺序) 💡应用场景:希尔排序在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序(O(logn))在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序 ,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法.
【优化3】希尔排序+二分查找:通过二分查找快速找到插入位置,减少比较次数
合并排序Merge sort 递归(top-down)-recursion
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public int [] mergeSort(int [] nums){ int [] temp = new int [nums.length]; sort(nums, 0 , nums.length-1 , temp); return nums; } private void sort (int [] nums, int lo, int hi, int [] temp) { if (lo >= hi) return ; int mid = lo + (hi - lo) / 2 ; sort(nums, lo, mid, temp); sort(nums, mid+1 , hi, temp); merge(nums, lo, hi, temp); } private void merge (int [] nums, int lo, int hi, int [] temp) { int mid = lo + (hi - lo) / 2 ; int i = lo; int j = mid+1 ; int k = lo; while (i <= mid && j <= hi){ if (nums[i] < nums[j]){ temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; } } while (i <= mid){ temp[k++] = nums[i++]; } while (j <= hi){ temp[k++] = nums[j++]; } for (int p = lo; p <= hi; p++){ nums[p] = temp[p]; } }
算法分析: 最好情况O($nlogn$)-初始序列已经是升序: $(nlogn)/2$次比较 最坏情况:O($nlogn$) 平均情况:O($nlogn$) 时间复杂度:O($nlogn$);空间复杂度O(n)(其中递归需要logn) 特点:稳定排序,任何情况都保证时复为O(nlogn)
【优化1】交换sort()里的数组,省去merge()里复制数组的步骤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void sort (int [] nums, int lo, int hi, int [] temp) { if (lo >= hi) return ; int mid = lo + (hi - lo) / 2 ; sort(temp, lo, mid, nums); sort(temp, mid+1 , hi, nums); merge(nums, lo, hi, temp); } private void merge (int [] nums, int lo, int hi, int [] temp) { int mid = lo + (hi - lo) / 2 ; int i = lo; int j = mid+1 ; int k = lo; while (i <= mid && j <= hi){ if (nums[i] < nums[j]){ temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; } } while (i <= mid){ temp[k++] = nums[i++]; } while (j <= hi){ temp[k++] = nums[j++]; } }
【优化2】若前半和后半sort完,整体已经是sorted,则不用再merge O(n): n-1次比较, why?
1 2 3 4 5 6 7 8 private void sort (int [] nums, int lo, int hi, int [] temp) { if (lo >= hi) return ; int mid = lo + (hi - lo) / 2 ; sort(nums, lo, mid, temp); sort(nums, mid+1 , hi, temp); if (nums[mid] < nums[mid+1 ]) return ; merge(nums, lo, hi, temp); }
【优化3】对于小的数组用插入排序,减少空间占用
1 2 3 4 5 6 7 8 9 10 11 private void sort (int [] nums, int lo, int hi, int [] temp) { if (lo >= hi) return ; if ((hi - lo + 1 ) <= CUTOFF){ insertionSort(nums, lo, hi); return ; } int mid = lo + (hi - lo) / 2 ; sort(nums, lo, mid, temp); sort(nums, mid+1 , hi, temp); merge(nums, lo, hi, temp); }
迭代(bottom-up)-iterative
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public int [] mergeSort(int [] nums){ int N = nums.length; int [] temp = new int [N]; for (int sz = 1 ; sz < N; sz *= 2 ){ for (int lo = 0 ; lo < N-sz; lo += sz+sz){ merge(nums, lo, Math.min(lo+sz+sz-1 ,N-1 ), temp); } } return nums; } private void merge (int [] nums, int lo, int hi, int [] temp) { int mid = lo + (hi - lo) / 2 ; int i = lo; int j = mid+1 ; int k = lo; while (i <= mid && j <= hi){ if (nums[i] < nums[j]){ temp[k++] = nums[i++]; } else { temp[k++] = nums[j++]; } } while (i <= mid){ temp[k++] = nums[i++]; } while (j <= hi){ temp[k++] = nums[j++]; } for (int p = lo; p <= hi; p++){ nums[p] = temp[p]; } }
快速排序Quick sort 选择pivot,比pivot小的排前面,比pivot大的排后面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public int [] quickSort(int [] nums){ qsort(nums, 0 , nums.length - 1 ); return nums; } private void qsort (int [] nums, int lo, int hi) { if (lo >= hi) return ; int pivot = partition(nums, lo, hi); qsort(nums, lo, pivot - 1 ); qsort(nums, pivot + 1 , hi); } private int partition (int [] nums, int lo, int hi) { int pivot = nums[hi]; int k = lo; for (int i = lo; i < hi; i++){ if (nums[i] < pivot){ swap(nums, k, i); k++; } } swap(nums, k, hi); return k; } private void swap (int [] nums, int i, int j) { ... }
算法分析: 最好情况O(nlogn)-每次选择的pivot都是中位数 最坏情况O(n^2)-每次选择的pivot都是最小/最大数 平均情况O(nlnn)
特点:不稳定,但是只要不是worst case,时复O(nlogn)比merge sort的快 😁优点:不占用额外的内存空间(in-place算法) 😢缺点:若遇到worst case则时复高达O($n^2$) 💡应用场景:通常是最优解法
快速选择Quick Select:常用于第k大/小的问题 利用快速排序中当每个iteration结束,pivot的位置就是其最终已排序位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public int findKthLargest (int [] nums, int k) { k = nums.length - k; int lo = 0 ; int hi = nums.length - 1 ; while (lo < hi){ int pivot = partition(nums, lo, hi); if (pivot == k) return nums[k]; else if (pivot < k) lo = pivot + 1 ; else hi = pivot - 1 ; } return nums[k]; } private int partition (int [] nums, int lo, int hi) { int pivot = nums[hi]; int k = lo; for (int i = lo; i < hi; i++){ if (nums[i] < pivot){ swap(nums, k, i); k++; } } swap(nums, hi, k); return k; } private void swap (int [] nums, int i, int j) { ... }
荷兰国旗问题:优化quick sort,使其支持duplicate keys,3-way partitioning 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void sort (int [] nums) { int lo = 0 ; int hi = nums.length - 1 ; int p = 0 ; while (p <= hi){ if (nums[p] == 0 ){ swap(nums, p, lo); lo++; p++; }else if (nums[p] == 2 ){ swap(nums, p, hi); hi--; }else { p++; } } }
桶排序Bucket sort:常用于top k问题 详见top k问题总结
堆排序Heap sort 推荐Youtube Back to Back SWE频道的讲解:https://www.youtube.com/watch?v=k72DtCnY4MU Binary Heap性质:k的父结点在k/2,子结点在2k+1和2k+2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public int [] heapSort(int [] nums){ int n = nums.length - 1 ; for (int i = (n-1 )/2 ; i >= 0 ; i--){ heapify(nums, n, i); } for (int i = n; i >= 0 ; i--){ swap(nums, 0 , i); heapify(nums, i, 0 ); } } private void heapify (int [] nums, int n, int i) { int max = i; int l = 2 * i + 1 ; int r = 2 * i + 2 ; if (l < n && nums[l] > nums[max]) max = l; if (r < n && nums[r] > nums[max]) max = r; if (max != i){ swap(nums, max, i); heapify(nums, n, max); } } private void swap (int [] nums, int i, int j) { ... }
Java里Arrays.sort()的实现 sort reference类型时,用merge sort。因为稳定,且保证时复O(nlogn) sort primitive类型时,用quick sort。因为quick sort在实际应用中用的内存更少、更快