Skip to content

Commit 0e21c56

Browse files
author
hunterhug
committed
quick sort fix again
1 parent cc3d234 commit 0e21c56

File tree

8 files changed

+199
-56
lines changed

8 files changed

+199
-56
lines changed

algorithm/sort/quick_sort.md

Lines changed: 135 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
5858
快速排序主要靠基准数进行切分,将数列分成两部分,一部分比基准数都小,一部分比基准数都大。
5959

60+
### 1.1 时间复杂度
61+
6062
在最好情况下,每一轮都能平均切分,每一轮遍历比较元素 `n` 次就可以把数列分成两部分,每一轮的时间复杂度都是:`O(n)`。因为问题规模每次被折半,折半的数列继续递归进行切分,也就是总的时间复杂度计算公式为: `T(n) = 2*T(n/2) + O(n)`。按照主定理公式计算,我们可以知道时间复杂度为:`O(nlogn)`,当然我们可以来具体计算一下:
6163

6264
```
@@ -108,25 +110,33 @@ T(n) = T(n-1) + n
108110

109111
根据熵的概念,数量越大,随机性越高,越自发无序,所以待排序数据规模非常大时,出现最差情况的情形较少。在综合情况下,快速排序的平均时间复杂度为:`O(nlogn)`。对比之前介绍的排序算法,快速排序比那些动不动就是平方级别的初级排序算法更佳。
110112

111-
切分的结果极大地影响快速排序的性能,为了避免切分不均匀情况的发生,有几种方法改进:
113+
### 1.2 空间复杂度
112114

113-
1. 每次进行快速排序切分时,先将数列随机打乱,再进行切分,这样随机加了个震荡,减少不均匀的情况。当然,也可以随机选择一个基准数,而不是选第一个数。
114-
2. 每次取数列头部,中部,尾部三个数,取三个数的中位数为基准数进行切分。
115+
快速排序使用原地排序,存储空间复杂度为:`O(1)`。而因为递归栈的影响,递归的程序栈开辟的层数范围在 `logn ~ n`,所以递归栈的空间复杂度为:`O(logn) ~ log(n)`,最坏为:`log(n)`,当元素较多时,程序栈可能溢出。通过改进算法,使用伪尾递归进行优化,递归栈的空间复杂度可以减小到 `O(logn)`,可以见下面算法优化。
115116

116-
方法 1 相对好,而方法 2 引入了额外的比较操作,一般情况下我们可以随机选择一个基准数
117+
快速排序是不稳定的,因为切分过程中进行了交换,相同值的元素可能发生位置变化
117118

118-
快速排序使用原地排序,存储空间复杂度为:`O(1)`。而因为递归栈的影响,递归的程序栈开辟的层数范围在 `logn~n`,所以递归栈的空间复杂度为:`O(logn)~log(n)`,最坏为:`log(n)`,当元素较多时,程序栈可能溢出。通过改进算法,使用伪尾递归进行优化,递归栈的空间复杂度可以减小到 `O(logn)`,可以见下面算法优化。
119+
### 1.3 切分优化
119120

120-
快速排序是不稳定的,因为切分过程中进行了交换,相同值的元素可能发生位置变化。
121+
切分的结果极大地影响快速排序的性能,比如每次切分的时候选择的基数数都是数组中最大或者最小的,会出现最坏情况,为了避免切分不均匀情况的发生,有几种方法改进:
122+
123+
1. 随机基准数选择:每次进行快速排序切分时,先将数列随机打乱,再进行切分,这样随机加了个震荡,减少不均匀的情况。当然,也可以随机选择一个基准数,而不是选第一个数。
124+
2. 中位数选择:每次取数列头部,中部,尾部三个数,取三个数的中位数为基准数进行切分。
125+
126+
方法 1 相对好,而方法 2 引入了额外的比较操作,一般情况下我们可以随机选择一个基准数。
127+
128+
从一个数组中随机选择一个数,或者取中位数都比较容易实现,我们在此就不实现了,避免造成心智负担,下文代码实现都取第一个数为基准数。
121129

122130
## 二、算法实现
123131

132+
这是最普通的一种实现。
133+
124134
```go
125135
package main
126136

127137
import "fmt"
128138

129-
// 普通快速排序
139+
// QuickSort 普通快速排序
130140
func QuickSort(array []int, begin, end int) {
131141
if begin < end {
132142
// 进行切分
@@ -153,8 +163,8 @@ func partition(array []int, begin, end int) int {
153163
}
154164
}
155165

156-
/* 跳出while循环后,i = j。
157-
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
166+
/* 跳出 for 循环后,i = j。
167+
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
158168
* --> array[i+1] ~ array[end] > array[begin]
159169
* 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
160170
* 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!
@@ -207,11 +217,32 @@ func main() {
207217

208218
1. 在小规模数组的情况下,直接插入排序的效率最好,当快速排序递归部分进入小数组范围,可以切换成直接插入排序。
209219
2. 排序数列可能存在大量重复值,使用三向切分快速排序,将数组分成三部分,大于基准数,等于基准数,小于基准数,这个时候需要维护三个下标。
210-
3. 使用伪尾递归减少程序栈空间占用,使得栈空间复杂度从 `O(logn)~log(n)` 变为:`O(logn)`
220+
3. 使用伪尾递归减少程序栈空间占用,使得栈空间复杂度从 `O(logn) ~ log(n)` 变为:`O(logn)`
211221

212222
### 3.1 改进:小规模数组使用直接插入排序
213223

224+
在小规模数组的情况下,直接插入排序的效率最好,当快速排序递归部分进入小数组范围,可以切换成直接插入排序。
225+
214226
```go
227+
// InsertSort 改进:当数组规模小时使用直接插入排序
228+
func InsertSort(list []int) {
229+
n := len(list)
230+
// 进行 N-1 轮迭代
231+
for i := 1; i <= n-1; i++ {
232+
deal := list[i] // 待排序的数
233+
j := i - 1 // 待排序的数左边的第一个数的位置
234+
235+
// 如果第一次比较,比左边的已排好序的第一个数小,那么进入处理
236+
if deal < list[j] {
237+
// 一直往左边找,比待排序大的数都往后挪,腾空位给待排序插入
238+
for ; j >= 0 && deal < list[j]; j-- {
239+
list[j+1] = list[j] // 某数后移,给待排序留空位
240+
}
241+
list[j+1] = deal // 结束了,待排序的数插入空位
242+
}
243+
}
244+
}
245+
215246
func QuickSort1(array []int, begin, end int) {
216247
if begin < end {
217248
// 当数组小于 4 时使用直接插入排序
@@ -234,12 +265,14 @@ func QuickSort1(array []int, begin, end int) {
234265

235266
### 3.2 改进:三向切分
236267

268+
排序数列可能存在大量重复值,使用三向切分快速排序,将数组分成三部分,大于基准数,等于基准数,小于基准数,这个时候需要维护三个下标。
269+
237270
```go
238271
package main
239272

240273
import "fmt"
241274

242-
// 三切分的快速排序
275+
// QuickSort2 三切分的快速排序
243276
func QuickSort2(array []int, begin, end int) {
244277
if begin < end {
245278
// 三向切分函数,返回左边和右边下标
@@ -308,18 +341,20 @@ func partition3(array []int, begin, end int) (int, int) {
308341

309342
如果存在大量重复元素,排序速度将极大提高,将会是线性时间,因为相同的元素将会聚集在中间,这些元素不再进入下一个递归迭代。
310343

311-
三向切分主要来自荷兰国旗三色问题,该问题由 `Dijkstra` 提出。
312-
313-
![](../../picture/quick_sort_three_flag.png)
314-
315-
假设有一条绳子,上面有红、白、蓝三种颜色的旗子,起初绳子上的旗子颜色并没有顺序,您希望将之分类,并排列为蓝、白、红的顺序,要如何移动次数才会最少,注意您只能在绳子上进行这个动作,而且一次只能调换两个旗子。
316-
317-
可以看到,上面的解答相当于使用三向切分一次,只要我们将白色旗子的值设置为 `100`,蓝色的旗子值设置为 `0`,红色旗子值设置为 `200`,以 `100` 作为基准数,第一次三向切分后三种颜色的旗就排好了,因为 `蓝(0)白(100)红(200)`
318-
319-
注:艾兹格·W·迪科斯彻(`Edsger Wybe Dijkstra`,1930年5月11日~2002年8月6日),荷兰人,计算机科学家,曾获图灵奖。
344+
> 三向切分主要来自荷兰国旗三色问题,该问题由 `Dijkstra` 提出。
345+
>
346+
> ![](../../picture/quick_sort_three_flag.png)
347+
>
348+
>假设有一条绳子,上面有红、白、蓝三种颜色的旗子,起初绳子上的旗子颜色并没有顺序,您希望将之分类,并排列为蓝、白、红的顺序,要如何移动次数才会最少,注意您只能在绳子上进行这个动作,而且一次只能调换两个旗子。
349+
>
350+
>可以看到,上面的解答相当于使用三向切分一次,只要我们将白色旗子的值设置为 `100`,蓝色的旗子值设置为 `0`,红色旗子值设置为 `200`,以 `100` 作为基准数,第一次三向切分后三种颜色的旗就排好了,因为 `蓝(0)白(100)红(200)`
351+
>
352+
>注:艾兹格·W·迪科斯彻(`Edsger Wybe Dijkstra`,1930年5月11日~2002年8月6日),荷兰人,计算机科学家,曾获图灵奖。
320353
321354
### 3.3 改进:伪尾递归优化
322355

356+
使用伪尾递归减少程序栈空间占用,使得栈空间复杂度从 `O(logn) ~ log(n)` 变为:`O(logn)`
357+
323358
```go
324359
// 伪尾递归快速排序
325360
func QuickSort3(array []int, begin, end int) {
@@ -390,11 +425,11 @@ func QuickSort5(array []int) {
390425

391426
我们可以看到没有递归,程序栈空间复杂度变为了:`O(1)`,但额外的存储空间产生了。
392427

393-
辅助人工栈结构 `helpStack` 占用了额外的空间,存储空间由原地排序的 `O(1)` 变成了 `O(logn)~log(n)`
428+
辅助人工栈结构 `helpStack` 占用了额外的空间,存储空间由原地排序的 `O(1)` 变成了 `O(logn) ~ log(n)`
394429

395430
我们可以参考上面的伪尾递归版本,继续优化非递归版本,先让短一点的范围入栈,这样存储复杂度可以变为:`O(logn)`。如:
396431

397-
```
432+
```go
398433
// 非递归快速排序优化
399434
func QuickSort6(array []int) {
400435

@@ -467,20 +502,20 @@ import (
467502
"sync"
468503
)
469504

470-
// 链表栈,后进先出
505+
// LinkStack 链表栈,后进先出
471506
type LinkStack struct {
472507
root *LinkNode // 链表起点
473508
size int // 栈的元素数量
474509
lock sync.Mutex // 为了并发安全使用的锁
475510
}
476511

477-
// 链表节点
512+
// LinkNode 链表节点
478513
type LinkNode struct {
479514
Next *LinkNode
480515
Value int
481516
}
482517

483-
// 入栈
518+
// Push 入栈
484519
func (stack *LinkStack) Push(v int) {
485520
stack.lock.Lock()
486521
defer stack.lock.Unlock()
@@ -509,7 +544,7 @@ func (stack *LinkStack) Push(v int) {
509544
stack.size = stack.size + 1
510545
}
511546

512-
// 出栈
547+
// Pop 出栈
513548
func (stack *LinkStack) Pop() int {
514549
stack.lock.Lock()
515550
defer stack.lock.Unlock()
@@ -532,12 +567,12 @@ func (stack *LinkStack) Pop() int {
532567
return v
533568
}
534569

535-
// 栈是否为空
570+
// IsEmpty 栈是否为空
536571
func (stack *LinkStack) IsEmpty() bool {
537572
return stack.size == 0
538573
}
539574

540-
// 非递归快速排序
575+
// QuickSort5 非递归快速排序
541576
func QuickSort5(array []int) {
542577

543578
// 人工栈
@@ -570,7 +605,7 @@ func QuickSort5(array []int) {
570605
}
571606
}
572607

573-
// 非递归快速排序优化
608+
// QuickSort6 非递归快速排序优化
574609
func QuickSort6(array []int) {
575610

576611
// 人工栈
@@ -646,8 +681,8 @@ func partition(array []int, begin, end int) int {
646681
}
647682
}
648683

649-
/* 跳出while循环后,i = j。
650-
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
684+
/* 跳出 for 循环后,i = j。
685+
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
651686
* --> array[i+1] ~ array[end] > array[begin]
652687
* 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
653688
* 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!
@@ -695,7 +730,67 @@ func main() {
695730

696731
对栈,存储空间有要求的可以使用堆排序,比如 `Linux` 内核栈小,快速排序占用程序栈太大了,使用快速排序可能栈溢出,所以使用了堆排序。
697732

698-
`Golang` 中,标准库 `sort` 中对切片进行稳定排序:
733+
## 六、补充:Golang 内置排序库 sort
734+
735+
`Golang` 中,标准库 `sort` 中使用了多种排序算法,值得研究。
736+
737+
例子:
738+
739+
```go
740+
package main
741+
742+
import (
743+
"fmt"
744+
"sort"
745+
)
746+
747+
func InnerSort() {
748+
list := []struct {
749+
Name string
750+
Age int
751+
}{
752+
{"A", 75},
753+
{"B", 4},
754+
{"C", 5},
755+
{"D", 5},
756+
{"E", 2},
757+
{"F", 5},
758+
{"G", 5},
759+
}
760+
761+
sort.SliceStable(list, func(i, j int) bool { return list[i].Age < list[j].Age })
762+
fmt.Println(list)
763+
764+
list2 := []struct {
765+
Name string
766+
Age int
767+
}{
768+
{"A", 75},
769+
{"B", 4},
770+
{"C", 5},
771+
{"D", 5},
772+
{"E", 2},
773+
{"F", 5},
774+
{"G", 5},
775+
}
776+
777+
sort.Slice(list2, func(i, j int) bool { return list2[i].Age < list2[j].Age })
778+
fmt.Println(list2)
779+
}
780+
781+
func main() {
782+
InnerSort()
783+
}
784+
```
785+
786+
输出:
787+
788+
```
789+
[{E 2} {B 4} {C 5} {D 5} {F 5} {G 5} {A 75}]
790+
[{E 2} {B 4} {G 5} {C 5} {D 5} {F 5} {A 75}]
791+
```
792+
793+
其中 `SliceStable` 是稳定排序,使用了插入排序和归并排序:
699794

700795
```go
701796
func SliceStable(slice interface{}, less func(i, j int) bool) {
@@ -728,9 +823,9 @@ func stable_func(data lessSwap, n int) {
728823
}
729824
```
730825

731-
会先按照 `20` 个元素的范围,对整个切片分段进行插入排序,因为小数组插入排序效率高然后再对这些已排好序的小数组进行归并排序。其中归并排序还使用了原地排序,节约了辅助空间。
826+
会先按照 `20` 个元素的范围,对整个切片分段进行插入排序,因为小数组插入排序效率高然后再对这些已排好序的小数组进行归并排序。其中归并排序还使用了原地排序,节约了辅助空间。
732827

733-
而一般的排序:
828+
`Slice` 是一般的排序,不追求稳定排序,使用了快速排序:
734829

735830
```go
736831
func Slice(slice interface{}, less func(i, j int) bool) {
@@ -830,6 +925,10 @@ func doPivot_func(data lessSwap, lo, hi int) (midlo, midhi int) {
830925
}
831926
```
832927

833-
快速排序限制程序栈的层数为: `2*ceil(log(n+1))`,当递归超过该层时表示程序栈过深,那么转为堆排序。
928+
快速排序限制程序栈的层数为: `2 * ceil( log(n+1) )`,当递归超过该层时表示程序栈过深,内部会转为堆排序。
929+
930+
上述快速排序还使用了三种优化,第一种是递归时小数组转为插入排序,第二种是使用了中位数基准数,第三种使用了三向切分。
931+
932+
## 附录
834933

835-
上述快速排序还使用了三种优化,第一种是递归时小数组转为插入排序,第二种是使用了中位数基准数,第三种使用了三切分
934+
代码下载: [https://github.com/hunterhug/goa.c/tree/master/code/sort/quicksort](https://github.com/hunterhug/goa.c/tree/master/code/sort/quicksort)

code/sort/quicksort/c.cpp renamed to code/sort/quicksort/example/c.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void quicksort(int array[], int maxlen, int begin, int end)
7575
}
7676

7777
/* 跳出while循环后,i = j。
78-
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
78+
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
7979
* --> array[i+1] ~ array[end] > array[begin]
8080
* 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
8181
* 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!

code/sort/quicksort/main.go renamed to code/sort/quicksort/example/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package main
22

33
import "fmt"
44

5-
// 普通快速排序
5+
// QuickSort 普通快速排序
66
func QuickSort(array []int, begin, end int) {
77
if begin < end {
88
// 进行切分
@@ -29,8 +29,8 @@ func partition(array []int, begin, end int) int {
2929
}
3030
}
3131

32-
/* 跳出while循环后,i = j。
33-
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
32+
/* 跳出 for 循环后,i = j。
33+
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
3434
* --> array[i+1] ~ array[end] > array[begin]
3535
* 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
3636
* 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!

code/sort/quicksort/main1.go renamed to code/sort/quicksort/example1/main1.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package main
22

33
import "fmt"
44

5-
// 改进:当数组规模小时使用直接插入排序
5+
// InsertSort 改进:当数组规模小时使用直接插入排序
66
func InsertSort(list []int) {
77
n := len(list)
88
// 进行 N-1 轮迭代
@@ -53,8 +53,8 @@ func partition(array []int, begin, end int) int {
5353
}
5454
}
5555

56-
/* 跳出while循环后,i = j。
57-
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
56+
/* 跳出 for 循环后,i = j。
57+
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
5858
* --> array[i+1] ~ array[end] > array[begin]
5959
* 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
6060
* 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!

code/sort/quicksort/main2.go renamed to code/sort/quicksort/example2/main2.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package main
22

33
import "fmt"
44

5-
// 三切分的快速排序
5+
// QuickSort2 三切分的快速排序
66
func QuickSort2(array []int, begin, end int) {
77
if begin < end {
88
// 三向切分函数,返回左边和右边下标

code/sort/quicksort/main3.go renamed to code/sort/quicksort/example3/main3.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package main
22

33
import "fmt"
44

5-
// 伪尾递归快速排序
5+
// QuickSort3 伪尾递归快速排序
66
func QuickSort3(array []int, begin, end int) {
77
for begin < end {
88
// 进行切分
@@ -36,8 +36,8 @@ func partition(array []int, begin, end int) int {
3636
}
3737
}
3838

39-
/* 跳出while循环后,i = j。
40-
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
39+
/* 跳出 for 循环后,i = j。
40+
* 此时数组被分割成两个部分 --> array[begin+1] ~ array[i-1] < array[begin]
4141
* --> array[i+1] ~ array[end] > array[begin]
4242
* 这个时候将数组array分成两个部分,再将array[i]与array[begin]进行比较,决定array[i]的位置。
4343
* 最后将array[i]与array[begin]交换,进行两个分割部分的排序!以此类推,直到最后i = j不满足条件就退出!

0 commit comments

Comments
 (0)