文章目录
算法稳定性,时间复杂度,空间复杂度等算法基础知识:经典算法----基础知识
冒泡排序:基础算法----冒泡排序
选择排序:基础算法----选择排序
插入排序:基础算法----插入排序
快速排序:基础算法----快速排序
希尔排序:基础算法----希尔排序
归并排序:基础算法----归并排序
计数排序:基础算法----计数排序
桶排序: 基础算法----桶排序
基数排序:基础算法----基数排序
堆排序: 基础算法----堆排序
一 算法简介
选取一个基准元素(通常是序列的第一个或最后一个元素)作为基准值,小于基准值的元素移到基准值的左边,大于或等于基准值的元素移到基准值的右边
然后分别对左、右子序列递归排序,当左、右子序列排序完成(长度小于等于1),整个序列的排序完成
快速排序是一种基于分治思想的比较排序算法
二 时间复杂度,空间复杂度
一) 时间复杂度
每一趟都需要比较n-1次,每一趟结束后把原序列分成两部分:X1,X2
时间复杂度 T(n) = D(n) + T(X1) + T(X2)
其中D(n) = n-1,表示每一趟比较的次数
最好情况
每次取的基准值将当前序列分为长度接近的两个子序列,经过log 2n趟划分,即可得到长度为1的子序列,时间复杂度为O(nlog 2n)
T(n) = D(n) + 2T(n/2)
= D(n) + 2D(n/2) + 4T(n/4)
= D(n) + 2D(n/2) + 4D(n/4) + 8T(n/8)
= D(n) + 2D(n/2) + … + 2kD(n/2k)
其中:
D(n) = n -1,表示每一趟比较的次数
k = log2n,经过log2n趟划分,即可得到长度为1的子序列
所以:
T(n) = n-1 + n-2 + … + n - 2k
= nlog2n - 2n +1
最坏情况
每次取的基准值是最大或最小,一个子序列长度为0,一个子序列长度为原序列长度-1, 时间复杂度为O(n 2)
T(n) = D(n) + T(n-1)
= D(n) + D(n-1) + T(n-2)
= D(n) + D(n-1) + D(n-2)+ T(n-3)
= D(n) + D(n-1) +D(n-2)+ D(n-3) + … + D(2) + D(1)
= n(n-1)/2
平均时间复杂度为 T(n) = O(nlog2n)
二) 空间复杂度
快速排序使用原地排序,存储空间复杂度为:S(1)
由于需要递归,程序栈层数范围在 S(n) = O(log2n)~O(n)
快速排序的空间复杂度为:S(n) = O(log2n)~O(n)
三 算法稳定性
与基准值相同的元素,因为分区而导致顺序不一致,如:{4, 2, 4, 5, 1, 6, 7},选择第三个元素作为基准值时,第一个元素4因为分区移动到基准值的右边
快速排序是一种不稳定的排序算法
四 基于golang代码的实现
一) 基础递归版
1 步骤
1) 选取第一个元素作为基准值(arr[0])
2) 从第二个元素(i = 1)开始从前往后遍历序列,循环比较,小于基准值的移到左边,大于等于基准值的移到右边。其中:小于基准值的元素移到左边的顺序是索引从小到大,大于等于基准值的元素移到右边的顺序是索引从大到小
3) 左、右子序列递归排序
2 过程图
使用 {40, 30, 50, 40, 20, 60, 10, 70} 演示从小到大的排序过程
其中
1) 原始序列拆分左、右子序列
2) 递归左子序列
2.1) 左子序列的左子序列
2) 递归右子序列
2.1) 右子序列的左子序列
3 代码实现
func QuickSort(arr []int) {
if len(arr) <= 1 {
return
}
head, tail := 0, len(arr)-1
for i := 1; head < tail; {
if arr[i] < arr[head] {
arr[i], arr[head] = arr[head], arr[i]
head++
i++
} else {
arr[i], arr[tail] = arr[tail], arr[i]
tail--
}
}
QuickSort(arr[:head])
QuickSort(arr[head+1:])
return
}
其中
head:存放小于基准值的位置(索引,初值为0)
tail:存放大于或等于基准值的位置(索引,初值为序列长度-1)
如果当前元素(arr[i])小于基准值,交换位置到基准值的左边,每交换一次,head+1,即[0: head]之间的元素已经小于基准值
如果当前元素(arr[i])大于或等于基准值,交换位置到基准值的右边,每交换一次,tail-1,即[tail: 序列长度-1]之间的元素已经大于或等于基准值
当前元素(arr[i])大于或等于基准值,不用 i++,因为交换后的arr[i](arr[i]=arr[tail])不一定大于等于基准值,需要再次进行比较
二) hoare法
1 步骤
1) 选取第一个元素作为基准值(arr[0])
2) 定义两个变量: head, tail,tail从右向左移动,找到比基准值小的元素停下;head从左向右移动,找到比基准值大的元素停下;然后交换head, tail的位置
3) 重复步骤2),直到head和tail相遇
4) 交换基准值和相遇的位置
5) 左、右子序列递归排序
2 过程图
使用 {40, 30, 50, 40, 20, 60, 10, 70} 演示从小到大的排序过程
1) 原始序列拆分左、右子序列
![hoare法----原始序列拆分左、右子序列](https://i-blog.csdnimg.cn/blog_migrate/52a7615a52de48d761c36ffb9aa47709.png)
2) 递归左子序列
2.1) 左子序列的右子序列
![hoare法----左子序列的右子序列](https://i-blog.csdnimg.cn/blog_migrate/6436229636c391e9ff3c50ded251e89e.png)
3) 递归右子序列
3 代码实现
func QuickSort(arr []int) {
if len(arr) <= 1 {
return
}
baseIndex, head, tail := 0, 0, len(arr)-1
for head < tail {
//从右往左找第一个小于基准值的元素
for head < tail && arr[tail] >= arr[baseIndex] {
tail--
}
//从左往右找第一个大于基准值的元素
for head < tail && arr[head] <= arr[baseIndex] {
head++
}
//大于基准值的元素和小于基准值的元素交换位置
arr[head], arr[tail] = arr[tail], arr[head]
}
//交换baseIndex和head, tail相遇位置的元素
arr[baseIndex], arr[head] = arr[head], arr[baseIndex]
QuickSort(arr[:head])
QuickSort(arr[head+1:])
return
}
三) 挖坑法
挖坑法思路与hoare法思路大致相同
1 步骤
1) 选取第一个元素作为基准值(arr[0]),保存到变量中,该位置类似被挖了一个坑
2) 定义两个变量: head, tail,tail从右向左移动,找到比基准值小的元素停下,把找到的元素填入坑中,找到的元素的位置又形成一个新的坑
3) head从左向右移动,找到比基准值大的元素停下,把找到的元素填入坑中,找到的元素的位置又形成一个新的坑
4) 重复步骤2)、3),直到head和tail相遇
5) 基准值填入相遇的位置
6) 左、右子序列递归排序
2 过程图
使用 {40, 30, 50, 40, 20, 60, 10, 70} 演示从小到大的排序过程
1) 原始序列拆分左、右子序列
2) 递归左子序列
![挖坑法----递归左子序列](https://i-blog.csdnimg.cn/blog_migrate/7d117b169c596f8c93a46e2a62496191.png)
2.1) 左子序列的右子序列
3) 递归右子序列
3 代码实现
func QuickSort(arr []int) {
if len(arr) <= 1 {
return
}
//基准值
baseValue := arr[head]
baseIndex, head, tail := 0, 0, len(arr)-1
for head < tail {
//从右往左找第一个小于基准值的元素
for head < tail && arr[tail] >= baseValue {
tail--
}
//找到的元素填入坑中
arr[baseIndex] = arr[tail]
//更新坑, 新坑就是找到的元素的位置
baseIndex = tail
//从左往右找第一个大于基准值的元素
for head < tail && arr[head] <= baseValue {
head++
}
//找到的元素填入坑中
arr[baseIndex] = arr[head]
//更新坑, 新坑就是找到的元素的位置
baseIndex = head
}
//基准值填入head, tail相遇的位置
arr[baseIndex] = baseValue
QuickSort(arr[:head])
QuickSort(arr[head+1:])
}