排序算法总结
根据袁厨的算法小屋排序部分做的总结笔记
冒泡排序
两两比较相邻记录的关键字,如果反序则交换,直到没有反序为止。
冒泡一次会让至少一个元素移动到它应该在的位置,那么如果数组有n个元素,重复n次后则能完成排序。根据定义可知冒泡排序是比较类排序
//冒泡
func bubbleSort(nums: inout [Int]) {
//增加标记防止无意义的遍历
var flag = true
var i = 0
while i < nums.count && flag == true {
flag = false
for j in 0..<nums.count - i - 1 { //注意边界
if nums[j] > nums[j+1] {
swap(nums: &nums, i: j, j: j+1)
flag = true
}
}
i += 1
}
}
算法名称 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
简单选择排序
主要思路是通过在n-i+1个记录中选取关键字最小的记录作为有序序列的第i个记录,
//简单选择排序
func selectSort(nums: inout [Int]) {
guard nums.count > 0 else {
return
}
var min = 0
for i in 0 ..< nums.count {
min = i
for j in (i+1)..<nums.count {
if nums[min] > nums[j] {
min = j
}
}
if min != i {
swap(nums: &nums, i: i, j: min)
}
}
}
算法名称 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
直接插入排序
将一个记录插入到已经排好序的有序表中,从而得到一个新的有序表。通俗地讲,我们首先将序列分为两个区间,有序区间和无序区间,我们每次在无序区间内取一个值,在已排序区间中找到合适的位置插入,并保证有序区间一直有序
//直接插入排序
func straightInsertionSort(nums: inout [Int]) {
guard nums.count > 0 else {
return
}
for i in 0..<nums.count {
let temp = nums[i]//待排序的值
var j = i-1 //有序数组的最后一个值,就是gif中的数字5
while j >= 0 {
if temp < nums[j] {
nums[j+1] = nums[j] //把大的往后挪
j -= 1
continue
}
break
}
//插入到合适位置,这也就是我们没有在 for 循环内定义变量的原因
nums[j+1] = temp
}
}
直接插入排序
希尔排序
是插入排序的一种,又称“缩小增量排序”,是一个有跨度的插入排序,这个跨度会逐渐变小,变为 1 时记录也就基本有序,这时用到的也就是我们之前讲的直接插入排序了。
//希尔排序
func shellSort(nums: inout [Int]) {
//增量
var increment = nums.count
while increment > 1 {
increment = increment / 2
for i in 0..<increment {
//插入排序
for j in stride(from: i+increment, to: nums.count, by: increment) {
let temp = nums[j]
var k = j - increment
while k >= 0 {
if temp < nums[k] {
nums[k + increment] = nums[k]
k -= increment
continue
}
break
}
nums[k+increment] = temp
}
}
}
}
希尔排序
归并排序
归并排序的核心:分治法
将数组分割成两部分(左边可能比右边多一个)
继续将数组分割,直到所有部分的元素个数为1,
从最底层开始逐步合并两个排好序的数列。
知乎上这片文章讲的通俗易懂
//归并排序
func mergeSort(nums: inout [Int]) {
guard nums.count > 0 else {
return
}
var temp = Array(repeating: 0, count: nums.count)
mergeSort(nums: &nums, left: 0, right: nums.count-1, temp: &temp)
}
func mergeSort(nums: inout [Int], left: Int, right: Int, temp: inout [Int]) {
if left < right {
let mid = left + ((right - left) >> 1)
mergeSort(nums: &nums, left: left, right: mid, temp: &temp) //归并左半部分
mergeSort(nums: &nums, left: mid+1, right: right, temp: &temp) //归并右半部分
merge(nums: &nums, left: left, mid: mid, right: right, temp: &temp) //最后归并左右部分
}
}
/**
合并两个有序数列
nums[left]~nums[mid]为第一组
nums[mid+1]~nums[right]为第二组
temp是存放两组比较结果的临时数组
*/
func merge(nums: inout [Int], left: Int, mid: Int, right: Int, temp: inout [Int]) {
var i = left, j = mid + 1 //i 为第一组的起点,j为第二组的起点
let m = mid, n = right //m 为第一组的终点,n为第二组的终点
var k = 0 //k是temp数组当前的位置
while i <= m && j <= n {
if nums[i] <= nums[j] { //两组进行比较
temp[k] = nums[i] //如果第一组的数据小,把nums[i]放入temp,同时移动k和i的指针
k += 1
i += 1
} else {
temp[k] = nums[j] //如果第二组的数据小,把nums[j]放入temp,同时移动k和j的指针
k += 1
j += 1
}
}
while i <= m { //如果比较完成后第一个数组还有剩余的部分,全部填入temp
temp[k] = nums[i]
k += 1
i += 1
}
while j <= n { //如果比较完成后第二个数组还有剩余的部分,全部填入temp
temp[k] = nums[j]
k += 1
j += 1
}
for s in 0..<k { //将临时数组数据回填到原始数组
nums[left+s] = temp[s]
}
}
算法名称 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序
基本思路
1.找一个数组中的基准数
(1).一般数组的第一个
(2).为了防止出现最大数出现在头部的情况,采用三数取中的方式
2.让其他比它大的元素移动到数列一边,比他小的元素移动到数列另一边,从而把数组拆解成两个部分
3.再对左右部分,分别重复第二步,直到每部分元素个数为1
快排分类
值得注意的是,根据使用的指针数量,将排序的分为三种
1.单路快排:使用一个指针,移动指针并且判断当前元素和pivot的大小,进行交换
2.双路快排:使用两个指针,找到右指针小于pivot的元素,找到左指针大于pivot的元素,将右指针和左指针的元素交换,最后再将基准数pivot归位
3.三路快排:使用三个指针,分别是左指针left,右指针right和探路指针i;移动i,并且和pivot判断:
(1)<pivot,交换i和left的值,并且left++,i++
(2)>pivot,交换i和right的值,并且right--
(3)=pivot,i++
挖坑法
1.将基准数pivot从数组中取出;(挖出pivot)
2.判断nums[high] < pivot ,如果条件不成立使high--,直到nums[high] < pivot,将nums[high]填入到之前的位置;(挖出high,填入之前的坑位)
3.判断nums[low] > pviot,如果条件不成立使low++,直到nums[low] > pivot,将nums[low]填入到之前high的位置;(挖出low,填入之前high的坑位)
4.最后将pviot填入到之前low的位置。(回填)
func quickSort(nums: inout [Int], low: Int, high: Int) {
if low < high {
let index = partition1(nums: &nums, low: low, high: high)
quickSort(nums: &nums, low: low, high: index)
quickSort(nums: &nums, low: index + 1, high: high)
}
}
//挖坑法
func partition1(nums: inout [Int], low: Int, high: Int) -> Int {
//三数取中
let mid = low + ((high - low) >> 1)
if nums[low] > nums[high] {swap(nums: &nums, i: low, j: high)}
if nums[mid] > nums[high] {swap(nums: &nums, i: mid, j: high)}
if nums[mid] > nums[low] {swap(nums: &nums, i: mid, j: low)}
let pivot = nums[low] //取出pivot
var low = low
var high = high
while low < high {
//比较high
while low < high && nums[high] >= pivot {
high -= 1
}
//填坑
if low < high {nums[low] = nums[high]}
//比较low
while low < high && nums[low] <= pivot {
low += 1
}
//填坑
if low < high {nums[high] = nums[low]}
}
//将pivot填坑
nums[low] = pivot
return low
}
交换法
1.对上面方法的挖坑填坑步骤进行合并,
2.low 指针找到大于 pivot 的元素,hight 指针找到小于 pivot 的元素,然后两个元素交换位置
3.最后再将基准数归位
//交换法
func partition2(nums: inout [Int], low: Int, high: Int) -> Int {
//三数取中
let mid = low + ((high - low) >> 1)
if nums[low] > nums[high] {swap(nums: &nums, i: low, j: high)}
if nums[mid] > nums[high] {swap(nums: &nums, i: mid, j: high)}
if nums[mid] > nums[low] {swap(nums: &nums, i: mid, j: low)}
let pivot = nums[low]
let lowLet = low //保存low原始值
var low = low
var high = high
while low < high {
while low < high && nums[high] >= pivot {
high -= 1
}
while low < high && nums[low] <= pivot {
low += 1
}
if low >= high {//退出条件
break
}
swap(nums: &nums, i: low, j: high) //交换low和high
}
swap(nums: &nums, i: lowLet, j: low) //基准数归位
return low
}
func swap(nums: inout [Int], i: Int, j: Int) {
if i == j {
//应当注意,由于这里直接操作的是对象类型,不是值类型,所以如果i=j时进行异或操作会导致i为0
//所以这里需要进行判断
return
}
nums[i] ^= nums[j]
nums[j] ^= nums[i]
nums[i] ^= nums[j]
}
三向切分(三路快排)
作用:当数组重复元素比较多时,减小递归调用时的区间大小
1.定义探路指针i = low+1,
2.当nums[i] < pivot时,交换i和low的值,移动low +1 ,移动 i +1
3.当nums[i] > pivot时,交换i和high的值,移动high -1,
4.当nums[i] == pivot时,只移动i +1
//三向切分
func partition3(nums: inout [Int], low: Int, high: Int) -> Int {
//三数取中
let mid = low + ((high - low) >> 1)
if nums[low] > nums[high] {swap(nums: &nums, i: low, j: high)}
if nums[mid] > nums[high] {swap(nums: &nums, i: mid, j: high)}
if nums[mid] > nums[low] {swap(nums: &nums, i: mid, j: low)}
let pivot = nums[low]
var low = low
var high = high
var i = low + 1
while i <= high {
if nums[i] < pivot { //如果nums[i] < pivot,交换i和low的值,并且移动low和i
swap(nums: &nums, i: i, j: low)
low += 1
i += 1
} else if nums[i] > pivot { //如果nums[i] > pivot,交换i和high的值,并且移动high
swap(nums: &nums, i: i, j: high)
high -= 1
} else { //如果nums[i] == pivot,只移动i
i += 1
}
}
return low
}
func swap(nums: inout [Int], i: Int, j: Int) {
if i == j {
//应当注意,由于这里直接操作的是对象类型,不是值类型,所以如果i=j时进行异或操作会导致i为0
//所以这里需要进行判断
return
}
nums[i] ^= nums[j]
nums[j] ^= nums[i]
nums[i] ^= nums[j]
}
图示
挖坑法.png交换法
三向切分.png
堆排序
在堆中计算二叉树节点的左右节点以及父子节点
某节点下标为i(非叶子节点)
左子节点:2*i
右子节点:2*i+1
父节点:i/2
堆排序步骤
1.建堆
(1).大顶堆,小顶堆
(2).上浮法,下沉法(性能较好)
2.排序
建堆
上浮法
以小顶堆为例
给定一个节点,与父节点判断大小,如果小于父节点,交换两个节点的位置,不断进行操作,直到上浮到合适位置
//上浮法建小顶堆
func swim(nums: inout [Int], index: Int) {
var index = index
while index > 1 && nums[index/2] > nums[index] {
swap(nums: &nums, i: index/2, j: nums[index])
index /= 2
}
}
下沉法
以大顶堆为例
找到最后一个非叶子节点(len/2),首先获取左子节点(index*2),判断和右子节点(index*2+1),找到最大的那个,然后和当前节点进行判断,如果当前节点较小,则交换
为什么是找最大的那个呢?
因为大顶堆是每个节点的子节点必须小于父节点,如果和最小的交换,是无法保证的(如果是小顶堆应该和最小的交换)
//下沉法建大顶堆
func sink(nums: inout [Int], index: Int, len: Int) {
var index = index
while 2 * index <= len {
var j = 2 * index //左子节点
if j+1 <= len && nums[j+1] > nums[j] { //与右子节点判断,如果右子节点大,切换到右子节点
j += 1
}
//判断与子节点大小,如果子节点大于当前节点,则交换,否则退出
if nums[j] > nums[index] {
swap(nums: &nums, i: j, j: index)
} else {
break
}
index = j//如果还有子节点,继续下沉
}
}
排序
排序的思路:
1.将堆顶的元素(最大的元素)和最后一个元素交换
2.堆新的堆顶元素进行下沉
3.遍历执行,直到k>1
//堆排序
func heapSort(nums: inout [Int]) {
let len = nums.count
var a = Array(repeating: -1, count: len + 1)
//集体移动一位,数组创建堆时第一位要空出
for i in 0..<len {
a[i+1] = nums[i]
}
//下沉法建大顶堆
for i in (1...(len/2)).reversed() {
sink(nums: &a, index: i, len: len)
}
//排序
var k = len
while k>1 {
swap(nums: &a, i: 1, j: k) //堆顶元素(最大元素)与最后一个交换
k-=1
sink(nums: &a, index: 1, len: k) //新的堆顶元素进行下沉
}
//重整数组
for i in 1..<a.count {
nums[i-1] = a[i]
}
}
总结
1.建堆,通过下沉操作建堆效率更高,具体过程是,找到最后一个非叶子节点,然后从后往前遍历执行下沉操作。
2.排序,将堆顶元素(代表最大元素)与最后一个元素交换,然后新的堆顶元素进行下沉操作,遍历执行上诉操作,则可以完成排序。
计数排序
1.求统计计数数组
2.根据统计计数数组求前缀和数组
3.反序遍历原数组,根据原数组的元素查询presum,查询到的presum的元素-1就是需要放入临时数组的位置,完成后presum相应位置的元素-1
4.应当注意数组空间的问题,比如[91,92,93,94],还应当注意负数问题[-3,-1,-1,-2,0],解决思路是遍历时元素下标减去最小值,
先不考虑数组空间和负数问题,代码⬇️
//计数排序
func countSort(nums: inout [Int]) {
//前缀和数组
var presum = Array(repeating: 0, count: nums.count)
//临时数组
var temps = Array(repeating: 0, count: nums.count)
//1.求统计次数数组
for num in nums {
presum[num] += 1
}
//2.求前缀和数组
for i in 1..<nums.count {
presum[i] = presum[i-1] + presum[i]
}
//3.开始排序
for num in nums.reversed() {
temps[presum[num]-1] = num
presum[num] -= 1
}
nums = temps
}
增加解决数组空间和负数问题的代码⬇️
//计数排序
func countSort(nums: inout [Int]) {
guard nums.count > 0 else {
return
}
var max = nums[0]
var min = nums[0]
//求出最大最小值
for num in nums {
if max < num {max = num}
if min > num {min = num}
}
let count = max - min + 1
//前缀和数组
var presum = Array(repeating: 0, count: count)
//临时数组
var temps = Array(repeating: 0, count: nums.count)
//1.求统计次数组
for num in nums {
presum[num-min] += 1
}
//2.求前缀和数组
for i in 1..<nums.count {
presum[i] = presum[i-1] + presum[i]
}
//3.开始排序
for num in nums.reversed() {
//查找presum,将其放入临时数组
temps[presum[num-min]-1] = num
//presum相应位置-1
presum[num-min] -= 1
}
nums = temps
}
计数排序.png
时间复杂度:O(N+K)
空间复杂度:O(n)