目录
[1. 排序引言](#1. 排序引言)
[2. 冒泡排序](#2. 冒泡排序)
[2.1 算法思想](#2.1 算法思想)
[2.2 代码实现](#2.2 代码实现)
[2.3 时空复杂度分析](#2.3 时空复杂度分析)
[3. 选择排序](#3. 选择排序)
[3.1 算法思想](#3.1 算法思想)
[3.2 代码实现](#3.2 代码实现)
[3.3 时空复杂度分析](#3.3 时空复杂度分析)
[4. 插入排序](#4. 插入排序)
[4.1 算法思想](#4.1 算法思想)
[4.3 代码实现](#4.3 代码实现)
[4.4 时空复杂度分析](#4.4 时空复杂度分析)
[5. 快速排序](#5. 快速排序)
[5.1 算法思想](#5.1 算法思想)
[5.2 代码实现](#5.2 代码实现)
[5.3 时空复杂度分析](#5.3 时空复杂度分析)
[6. 归并排序](#6. 归并排序)
[6.1 算法思想](#6.1 算法思想)
[6.2 代码实现](#6.2 代码实现)
[6.3 时空复杂度分析](#6.3 时空复杂度分析)
1. 排序引言
排序算法 是算法竞赛中的第一入门必会的算法,可能在语言里面内置好sort排序函数,但是在排序算法中的很多思想是值得我们去学习的,比如从快速排序里面学会如何进行分治 以及递归的实现。
2. 冒泡排序
冒泡排序是学习语言和算法中必会的一种算法,下面就由我来进行冒泡排序的分析与代码实现:
2.1 算法思想
对于一个无序数组,我们从索引0开始往右对比,如果当前数字比后一个数字大,就进行交换。
这样每次就可以将最大的放在最右边, 上一次对比的最右边的就不再参与下一次排序
因为有N个数,每一次可以将一个最大数排好序,最后一个数也就定好了,因此只需要N-1次,就能排好完整的序。
我们可以看看以下的图:
其实到这里,冒泡排序算法就已经很明确了,每次冒泡都能求出当前最大的数,并将其放在最右边。
2.2 代码实现
python
a = [6, 5, 4, 1, 3, 2]
n = len(a)
for i in range(n - 1):
for j in range(n - i - 1):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
print(a)
2.3 时空复杂度分析
时间复杂度:
每次需要比较进行n - i - 1次,也就是n - 1 、 n - 2 、 n - 3.....1次
一共要执行n - 1次, 大概估算也就是O(n²)
空间复杂度:
在原数组上面进行的操作,并没有开辟新的空间,所以为:O(1)
3. 选择排序
3.1 算法思想
每次从左往右开始找,找到最小的,然后与当前的索引的数进行交换,并索引加一
这样就能保证,每次都将最小的数排在最前面了。
3.2 代码实现
python
a = [6, 5, 4, 1, 3, 2]
n = len(a)
for i in range(n - 1):
minn = a[i]
index = i
for j in range(i + 1, n):
if a[j] < minn:
minn = min(minn, a[j])
index = j
a[i], a[index] = a[index], a[i]
print(a)
3.3 时空复杂度分析
时间复杂度:
每一次都要从左往右开始比较,从n - 1 次到1次,也就是n(n - 1) / 2次
一共要进行n - 1次,所以时间复杂度为:O(n²)
空间复杂度:
没有开辟额外空间,为:O(1)
4. 插入排序
4.1 算法思想
还是从左往右开始进行排序,当前这个数与前面的每一个数进行比较,如果当前的数比前一个数小,那么就一直往左边走,直到那个数比当前的数大为止。
到当前这个数的时候,前面的数其实已经排序好了,只需要找个合适的位置插入进行就好了。
4.3 代码实现
python
a = [6, 5, 4, 1, 3, 2 , 10]
n = len(a)
for i in range(1 , n):
now = a[i]
index = i
for j in range(i - 1 , -1 , -1):
if a[j] > now:
index = j
a[j + 1] = a[j]
else:
break
a[index] = now
print(a)
4.4 时空复杂度分析
时间复杂度:
插入排序比选择排序更加优化一点,但是最坏情况都是O(n²)
但是最好的情况下(已经有序),只需要O(n)就行了
空间复杂度:
没有开辟额外的数组,O(1)
5. 快速排序
5.1 算法思想
快速排序是基于分治算法实现的
分治:将一个大问题分解为多个小问题,分别解决这些小问题,然后将它们的解合并起来,从而得到大问题的解。通常,分治算法包含三个步骤:分解(Divide)、解决(Conquer)、合并(Combine)。
要想理解分治,首先得理解什么是递归?
递归:递归是指在解决问题的过程中调用自身的过程。在编程中,递归是一种常见的编程技巧,它通过将问题分解成更小的、类似的子问题来解决复杂的问题。
这是一个简单的递归函数:
python
def factorial(n):
# 基本情况
if n == 0:
return 1
# 递归情况
return n * factorial(n - 1)
不难发现,其实这就是求解阶乘的函数,f(n) = n * fn(n - 1),直到计算到最底层f(0) = 1 ,也就是0的阶乘,然后再不停地返回值,最终得到n的阶乘
当然,上面不理解的话,我们先可以看一下他的实现逻辑:
先进行分解,算到最底层之后,又从下面往上面推,最终算出f(4)的结果为24
了解什么是分治和递归之后,我们就可以开始愉快的快速排序啦~
快速排序基本步骤:
- 在数组中找一个基准值x, 一般是中间那个值
- 将数组分成两个部分:1. 小于等于x的那部分, 2. 大于x的那部分
- 对两边递归使用该策略
最重要的步骤其实是将数组分成两个部分:
- 设置基准值l
- 存放小于等于基准值的下标为:idx = l + 1
- 从l + 1到r 遍历
- 如果当前的a[i]<=l , a[i] , a[idx]互换,并且idx += 1
- 最后就交换idx - 1和 l (idx是刚好大于l的,所以要-1,因为前面执行过一次idx += 1),就能保证l的左边是小于等于基准值的 , 右边是大于基准值的
5.2 代码实现
python
a = [6, 5, 4, 1, 3, 2, 10]
n = len(a)
def fn(a, l, r):
# 基准值为:l
idx = l + 1 # 右边的索引
for i in range(l + 1, r + 1):
# 将小于基准值的方在左边 ,大于基准值的放在右边
if a[i] <= a[l]:
a[i], a[idx] = a[idx], a[i]
idx += 1
# 将基准值放在中间
a[idx - 1], a[l] = a[l], a[idx - 1]
# 返回基准值的位置
return idx - 1
def quick_sort(a, l, r):
if l > r:
return
mid = fn(a, l, r) # 分基准值为l,分成两部分,左边<=mid , 右边>mid
quick_sort(a, l, mid - 1) # 对左边处理
quick_sort(a, mid + 1, r) # 对右边处理
quick_sort(a, 0, n - 1)
print(a)
5.3 时空复杂度分析
时间复杂度:
在一般情况下,我们每次需要遍历分成两个部分,需要执行n次,每次都分成两个部分,相比线性时间,每次排序的都少了一半,于是就是Logn次
总时间复杂度大概在O(n * logn)
空间复杂度:
每次递归都是一次递归二叉树,消费的栈空间大概在O(logn)
6. 归并排序
6.1 算法思想
归并排序也是基于分治算法来的
只是归并排序是先递归,再进行合并
算法步骤:
- 先分成两个部分
- 每部分都处理成有序的
- 再将两个数组合并起来
6.2 代码实现
python
a = [6, 5, 4, 1, 3, 2, 10]
n = len(a)
def merge(a,b):
res = []
while len(a) != 0 and len(b)!=0:
if a[0] <= b[0]: # 将小的值先放入res
res.append(a.pop(0))
else:
res.append(b.pop(0))
# 将a,b剩下的值放进来
res.extend(a)
res.extend(b)
return res
def merge_sort(a):
if len(a) < 2:
return a
mid = len(a) // 2 # 每次分为两个部分
left = merge_sort(a[:mid]) # 对左边处理
right = merge_sort(a[mid:]) # 对右边处理
return merge(left , right) # 合并两部分
a = merge_sort(a)
print(a)
6.3 时空复杂度分析
时间复杂度:
归并排序与快速排序是类似的,都是O(n * logn)
空间复杂度:
归并排序每次都需要开辟一个新空间,所以为O(n)