快速排序法
思想是每次选取一个数,将它移动到数组之间,使其左边全部都是比它小的数,它的右边都是比它大的数
数组中选取一个数,与左边第一个数进行交换,从第二个数开始逐步排序(比它大的位置就不动,比它小的就换到前面,这样大的数也就自动的移动到了后面 ),最后在将第一个数与比它小的最后一个数进行交换使其移动到数组中间 ,使得左边的数都小于它,右边的数都大于它
存在问题
对于选取的数是左边第二个,如果是完全有序 的数组,则每次递归左边都是nil,右边是比原先数组少一个元素的数组,即会递归n次 此时时间复杂度为n² ,而栈的深度 也会为n,会导致栈溢出 的情况
解决方法
在partition 时选取[l,r]之间的随机数 ,防止是有序数组而导致算法性能退化
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 30 func sortQuick (arr []int ) { QuickSort(arr, 0 , len (arr)-1 ) }func QuickSort (arr []int , l, r int ) { if l >= r { return } p := partition(arr, l, r) QuickSort(arr, l, p-1 ) QuickSort(arr, p+1 , r) }func partition (arr []int , l, r int ) int { rand.Seed(time.Now().UnixNano()) v := rand.Intn(r-l+1 ) + l arr[v], arr[l] = arr[l], arr[v] j := l for i := l + 1 ; i <= r; i++ { if arr[i] < arr[l] { j++ arr[i], arr[j] = arr[j], arr[i] } } arr[l], arr[j] = arr[j], arr[l] return j }
双路快速排序法
存在问题
即使加入了随机数,但是在一个所有数相等 的数组中,快速排序法还是退化 成了n² 的复杂度
解决思路
使用双路快速排序法 目的是将数组元素尽可能的分散在标定元素两侧 目的是使得 arr[l+1,i-1]<=v** **arr[j+1,r]>=v
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 func partition2 (arr []int , l, r int ) int { rand.Seed(time.Now().UnixNano()) v := rand.Intn(r-l+1 ) + l arr[v], arr[l] = arr[l], arr[v] i := l + 1 j := r for ; ; { for ; i <= j && arr[i] < arr[l]; { i++ } for ; i <= j && arr[j] > arr[l]; { j-- } if i >= j { break } arr[i], arr[j] = arr[j], arr[i] i++ j-- } arr[l], arr[j] = arr[j], arr[l] return j }
复杂度 由于加入了随机数 和双路排序思想,则不能 找到一个100%恶化的数组
则此时双路快速排序法是一个随机算法 ,不能采用最差情况讨论复杂度,而是要看期望
其期望还是 nlogn 的复杂度
三路快速排序法
存在问题
在二路快排中,虽然相同元素的数组中的元素被均摊到了两边,但是可以不必要 再对相同元素 再进行排序
解决思路
目的是使得 arr[l+1,lt]<v** **arr[lt+1,i-1]=v** **arr[gt,r]>v
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 func QuickSort3way (arr []int , l, r int ) { if l >= r { return } v := rand.Intn(r-l+1 ) + l arr[v], arr[l] = arr[l], arr[v] lt := l i := l + 1 gt := r + 1 for ; i < gt; { if arr[i] < arr[l] { lt++ arr[lt], arr[i] = arr[i], arr[lt] i++ } else if arr[i] > arr[l] { gt-- arr[gt], arr[i] = arr[i], arr[gt] } else { i++ } } arr[l], arr[lt] = arr[lt], arr[l] QuickSort3way(arr, l, lt-1 ) QuickSort3way(arr, gt, r) }
堆排序法 将数组中的数加入堆复杂度是 nlogn
将堆中依次 ExtractMax 复杂度是 nlogn
总复杂度是 2nlogn
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 30 31 32 33 34 35 36 37 38 func HeapSort (arr []int ) { h := heap.NewMaxHeap(len (arr)) for _, v := range arr { h.Add(v) } for i := len (arr) - 1 ; i >= 0 ; i-- { arr[i] = h.ExtractMax() } }func (h *MaxHeap) ExtractMax () int { res := h.FindMax() h.Array.Swap(0 , h.Array.Length-1 ) h.Array.RemoveLast() h.shiftDown(0 ) return res }func (h *MaxHeap) shiftDown (k int ) { for leftChild(k) < h.Array.GetSize() { j := leftChild(k) if j+1 < h.Array.GetSize() && h.Array.Get(j) < h.Array.Get(j+1 ) { j = rightChild(k) } if h.Array.Get(j) <= h.Array.Get(k) { break } h.Array.Swap(k, j) k = j } }
优化 通过Heapify 将数组堆化 ,其时间复杂度 是 n
没有开辟额外空间,空间复杂度 是 1
思路是:
将堆化的数组第一个元素和最后一个元素进行交换 ,使得最后一个元素排序成功
此时前n-1个元素又不满足最大堆,则通过特殊的shiftDown 将前n-1个元素排序成堆
此时再将第一个元素和倒数第二个元素进行交换,直至排序成功
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 30 31 32 33 34 35 func HeapSort2 (arr []int ) { if len (arr) <= 1 { return } heap.Heapify(arr) for i := len (arr) - 1 ; i >= 0 ; i-- { arr[0 ], arr[i] = arr[i], arr[0 ] shiftDownArr(arr, 0 , i) } }func Heapify (arr []int ) { h := MaxHeap{*array.NewInArray(arr, len (arr))} for i := parent(len (arr) - 1 ); i >= 0 ; i-- { h.shiftDown(i) } }func shiftDownArr (arr []int , k, n int ) { for 2 *k+1 < n { j := 2 *k + 1 if j+1 < n && arr[j] < arr[j+1 ] { j++ } if arr[j] <= arr[k] { break } arr[k], arr[j] = arr[j], arr[k] k = j } }
希尔排序法
对元素间距为n/2 的所有数组做插入排序 ,对元素间距为n/4 的所有数组做插入排序 , … 对元素间距为1 进行插入排序
看上去有四重循环,实际上其因为数组会越来越有序,循环n的次数也会越来越少,其复杂度是大于nlogn而小于n² ,在数组小的时候其性能可能还会超越nlogn并且没有使用递归,仅依靠循环就完成了排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func ShellSort (arr []int ) { h := len (arr) / 2 for h >= 1 { for start := 0 ; start < h; start++ { for i := start + h; i < len (arr); i += h { for j := i; j-h >= 0 && arr[j] < arr[j-h]; j -= h { arr[j], arr[j-h] = arr[j-h], arr[j] } } } h /= 2 } }
优化 四重循环压缩成三重循环,但是复杂度并没有改变 不用分别对分开 的数组对分别使用插入排序,可以直接交替 对数组对使用插入排序
1 2 3 4 5 6 7 8 9 10 11 12 13 func ShellSort2 (arr []int ) { h := len (arr) / 2 for h >= 1 { for i := h; i < len (arr); i++ { for j := i; j-h >= 0 && arr[j] < arr[j-h]; j -= h { arr[j], arr[j-h] = arr[j-h], arr[j] } } h /= 2 } }
步长序列 步长序列不同 ,复杂度分析不同 ,因为步长序列是一个超参数
还没有证明出一个最好的步长序列使得希尔排序性能最好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func ShellSort3 (arr []int ) { h := 0 for h < len (arr) { h = 3 *h + 1 } for h >= 1 { for i := h; i < len (arr); i++ { for j := i; j-h >= 0 && arr[j] < arr[j-h]; j -= h { arr[j], arr[j-h] = arr[j-h], arr[j] } } h /= 3 } }
基于比较的排序算法总结 时空复杂度
快速排序法是一个随机算法 ,则时间复杂度要用期望
稳定性 如果元素只有一个域,稳定性没有意义
基础排序算法的稳定性
插入排序法中相同的元素没有机会 进行 “跳跃 “
高级排序算法的稳定性