常用的排序算法
以下是一些最基本的排序算法。虽然在python/ C++ 里可以通过 std::sort() 快速排序,而且刷题
时很少需要自己手写排序算法,但是熟习各种排序算法可以加深自己对算法的基本理解,以及解
出由这些排序算法引申出来的题目。
1.选择排序(Selection Sort)
先从 n 个数字中找到最小值 min1,如果最小值 min1 的位置不在数组的最左端(也就是 min1 不等于 arr[0]),则将最小值 min1 和 arr[0] 交换,接着在剩下的 n-1 个数字中找到最小值 min2,如果最小值min2 不等于 arr[1],则交换这两个数字,依次类推,直到数组 arr 有序排列。算法的时间复杂度为 O ( n 2 ) 。 O(n^2)。 O(n2)。
选择排序算法的原理如下:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
def selectSort(nums):
n = len(nums)
for i in range(n-1):
min_idx = i
for j in range(i+1,n):
if nums[j]<nums[min_idx]:
min_idx = j
nums[i],nums[min_idx] = nums[min_idx],nums[i]
return nums
2.冒泡排序(Bubble Sort)
首先从数组的第一个元素开始到数组最后一个元素为止,对数组中相邻的两个元素进行比较,如果位于数组左端的元素大于数组右端的元素,则交换这两个元素在数组中的位置。这样操作后数组最右端的元素即为该数组中所有元素的最大值。接着对该数组除最右端的 n-1 个元素进行同样的操作,再接着对剩下的 n-2 个元素做同样的操作,直到整个数组有序排列。算法的时间复杂度为 O ( n 2 ) 。 O(n^2)。 O(n2)。
冒泡排序算法的原理如下:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
def bubbleSort(nums):
n = len(nums)
for i in range(n):
for j in range(0,n-i-1):
if nums[j]>nums[j+1]:
nums[j],nums[j+1] = nums[j+1],nums[j]
return nums
3.插入排序(Insert Sort)
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。例如要将数组 arr=[4,2,8,0,5,1] 排序,可以将4看做是一个有序序列,将 [2,8,0,5,1] 看做一个无序序列。无序序列中 2 比 4 小,于是将 2 插入到 4 的左边,此时有序序列变成了 [2,4],无序序列变成了 [8,0,5,1]。无序序列中 8 比 4 大,于是将 8 插入到 4 的右边,有序序列变成了 [2,4,8],无序序列变成了 [0,5,1]。以此类推,最终数组按照从小到大排序。该算法的时间复杂度为 O ( n 2 ) 。 O(n^2)。 O(n2)。
插入排序算法的原理如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤 2~5。
def insertSort(nums):
n = len(s)
for i in range(1,n):
j = i
while j>0:
if nums[j]<nums[j-1]:
nums[j],nums[j-1] = nums[j-1],nums[j]
j-=1
return nums
4.希尔排序(Shell Sort)
希尔排序(Shell’s Sort)在插入排序算法的基础上进行了改进,算法的时间复杂度与前面几种算法相比有较大的改进,但希尔排序是非稳定排序算法。
其算法的基本思想是:先将待排记录序列分割成为若干子序列分别进行插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行一次直接插入排序。该算法时间复杂度为
O
(
n
l
o
g
n
)
。
O(n log n)。
O(nlogn)。
希尔排序算法的原理如下:
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样(竖着的元素是步长组成):
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后我们对每列进行排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ]。这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变为:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1为步长就是简单的插入排序了
def shellSort(nums):
n = len(nums)
gap = n//2
# gap变化到0之前,插入排序执行的次数
while gap >= 1:
# 按步长进行插入排序、与普通的插入排序的区别就是步长
for i in range(gap,n):
j = i
# 插入排序
while j>0:
if nums[j]<nums[j-gap]:
nums[j],nums[j-gap] = nums[j-gap],nums[j]
j -= gap
else:
break
# 缩短步长
gap//=2
return nums
5.归并排序(Merge Sort)
归并排序是采用分治法的一个非常典型的应用。
归并排序的思想就是先递归分解数组,再合并数组。将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
def mergeSort(nums):
n =len(nums)
if n<=1:
return nums
mid = n//2
nums_left = mergeSort(nums[:mid])
nunms_right = mergeSort(nums[mid:])
result = []
i,j = 0,0
while i<len(nums_left) and j<len(nums_right):
if nums_left[i]<nums_right[j]:
result.append(nums_left[i])
i+=1
else:
result.append(nums_right[j])
j+=1
# 如果左右数组长度不对称、直接将剩余的元素添加到result中
result+=nums_left[i:]
result+=nums_right[j:]
return result
6.快速排序(Quick Sort)
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序算法的原理如下:
- 从数列中挑出一个元素,称为"基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
def quickSort(nums,first,last):
if first >= last:
return
i,j = first,last
base = nums[first]
#两全等状态时,快速排序起始项一定要从非基数base侧开始,不然基数无法调换
while i < j: #快速排序起始项一定要从非基数base侧开始,不然基数无法调换
while i < j and nums[j] >= base: #注意一定要加'='号,不然会进左右呼唤的死循环
j -= 1
nums[i],nums[j] = nums[j],nums[i]
while i < j and nums[i] <= base: #注意一定要加'='号,不然会进左右呼唤的死循环
i += 1
nums[i],nums[j] = nums[j],nums[i]
quickSort(nums,first,i-1)
quickSort(nums,j+1,last)
练习
1. 桶排序(347)
题目描述:
给定一个数组,求前 k 个最频繁的数字。
解题思路:
桶排序的意思是为每个值设立一个桶,桶内记录这个值出现的次数(或其它属
性),然后对桶进行排序。针对样例来说,我们先通过桶排序得到三个桶 [1,2,3,4],它们的值分别
为 [4,2,1,1],表示每个数字出现的次数。
- 首先遍历一遍 得出数字和次数的关系(map)
- 然后定义一个数组(桶) 由于k不会大于数组长度。所以桶的长度len(nums)+1即可
- 第k(k-1, k-2, k-3 …)大的次数就是第k个桶。 值就是对应的数字。(tmp[v] = num)。考虑到可能出现相同频率的数字(例如 11122212364 1和2的频率相同),所以桶是个二维数组。用来存放频率相同的数字(tmp[v] = append(tmp[v], num))
- 最后倒着遍历桶,取出桶里第二维数组里的数字。取够k个就 return
- 这样下来相当于遍历了3遍。时间复杂度n
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
cnt = collections.Counter(nums)
bucket = dict()
for x,v in cnt:
if v not in bucket:
bucket[v]=[x]
else:
bucket[v].append(x)
ans = []
for v in range(len(nums)-k+1,-1,-1):
if v in bucket:
ans.expend(bucket[v])
if len(ans)>=k:
return ans[:k]
2. 根据字符出现频率排序(451)
题目描述:
给定一个字符串 s ,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。
解题思路:
与桶排序思路类似
def frequencySort(self, s: str) -> str:
cnt = collections.Counter(s)
bucket = dict()
ans = []
for x,v in cnt.items():
if v not in bucket:
bucket[v] = [x*v]
else:
bucket[v].append(x*v)
for v in range(len(s),0,-1):
if v in bucket:
ans.extend(bucket[v])
return ''.join(ans)
3.颜色分类(75)
题目描述:
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
解题思路:
使用指针 p0 来交换 0,p2 来交换 2。此时,p0 的初始值为 0,而 p2 的初始值为 n−1。在遍历的过程中,我们需要找出所有的 0 交换至数组的头部,并且找出所有的 2 交换至数组的尾部。
我们从左向右遍历整个数组,设当前遍历到的位置为 i,对应的元素为
n
u
m
s
[
i
]
{nums}[i]
nums[i]:
- 如果 nums[i] == 0, 将其与 n u m s [ p 0 ] {nums}[p_0] nums[p0]进行交换,并将 p 0 p_0 p0 向后移动一个位置
- 如果 nums[i] ==2,将其与 n u m s [ p 2 ] {nums}[p_2] nums[p2]进行交换,并将 p 2 p_2 p2 向前移动一个位置
ps:
对于第二种情况,当我们将
n
u
m
s
[
i
]
与
n
u
m
s
[
p
2
]
{nums}[i] 与 {nums}[p_2]
nums[i]与nums[p2]进行交换之后,新的
n
u
m
s
[
i
]
{nums}[i]
nums[i]可能仍然是 2,也可能是 0。然而此时我们已经结束了交换,开始遍历下一个元素
n
u
m
s
[
i
+
1
]
{nums}[i+1]
nums[i+1],不会再考虑
n
u
m
s
[
i
]
{nums}[i]
nums[i] 了,这样我们就会得到错误的答案。
因此,当我们找到 2 时,我们需要不断地将其与 n u m s [ p 2 ] {nums}[p_2] nums[p2]进行交换,直到新的 n u m s [ i ] {nums}[i] nums[i]不为 2。此时,如果 n u m s [ i ] {nums}[i] nums[i] 为 0,那么对应着第一种情况;如果 n u m s [ i ] {nums}[i] nums[i] 为 1,那么就不需要进行任何后续的操作
def sortColors(self, nums: List[int]) -> None:
p0,p2 = 0,len(nums)-1
i = 0
while i<=p2:
while i<=p2 and nums[i] == 2:
nums[i],nums[p2] = nums[p2],nums[i]
p2-=1
if nums[i]==0:
nums[i],nums[p0] = nums[p0],nums[i]
p0+=1
i+=1