主要是分享代码实现,算法细节还需要大家仔细研究
冒泡排序
一种简单的排序方法。它重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换。
走访数列的工作是重复地进行直到不需要进行交换,也就是说该数列已经排序完成。这个算法的名字由来是因为元素会慢慢“浮”到数列的顶端。
1.比较相邻的元素。如果该元素比后一个元素大,就进行交换
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该是最大的数
3.针对所有元素重复上述步骤,除了最后一个
4.重复1-3,直到排序完成
注:其实每一次循环都会有一个数排到对的位置,随着循环次数的增加,步骤3可以不用对所有的元素重复
# python3
def BubbleSort(nums: list) -> list:
for i in range(len(nums)): ## 有几个元素就要循环几次
for j in range(0,len(nums)-1-i):
if nums[j+1] < nums[j]: ## 升序
temp = nums[j+1]
nums[j+1] = nums[j]
nums[j] = temp
return nums
选择排序
选择排序和冒泡类似,可以看作是对冒泡排序的优化。
1.找到数组中最大的那个元素
2.将它和数组中第一个元素进行交换,如果第一个元素就是最大的元素就和自己交换
3.在剩下的元素中找到最大的元素,将它和第二个元素进行交换。如此反复,直到将整个数组排序。
4.这种方法叫做选择排序是因为它在不断的选择剩余元素中的最大者
注:最大可以改为最小
## python3
def ChoiceSort(nums:list) -> list:
if len(nums) == 0:
return nums
for i in range(len(nums)):
minIndex = i # 每个循环开始总是假设第一个数最小
for j in range(i,len(nums)):
if nums[j] < nums[minIndex]:
minIndex = j # 找到最小元素的下标
temp = nums[minIndex] ## 进行交换
nums[minIndex] = nums[i]
nums[i] = temp
return nums
插入排序
像打麻将一样,把摸到的麻将插入到合适的位置,适合近乎有序的数组。
对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
为了给要插入的元素腾出空间,我们需要将插入位置之后的已排序元素都向后移动一位。
插入排序所需的时间取决于输入元素的初始顺序,例如:对一个很大且元素已经近乎有序的数组经行排序会比随机顺序的数组或是逆序数组进行排序要快得多。
注:插入排序对于部分有序的数组十分高效,也适合小规模数组。
# python3
def InsertionSort(nums: list) -> list:
if len(nums) == 0:
return nums
currentValue = None # 当前待排序数据,该元素之前的元素已经被排过序
for i in range(len(nums)-1):
preIndex = i # 已被排序数据的索引
currentValue = nums[preIndex+1]
while preIndex >= 0 and currentValue < nums[preIndex]:
nums[preIndex+1] = nums[preIndex] # 后移
preIndex -= 1
# while 循环结束说明已经找到合适的位置
nums[preIndex+1] = currentValue
return nums
快速排序
快速排序是对冒泡排序的一种改进,也是采用分治法的典型应用。
首先任意选取一个数据作为关键数据,我们称为基准数。
然后将所有比它小的数都放到它的前面,所有比它大的数都放到它的后面,
这个过程称为一趟快速排序,也称分区操作。
通过一趟快速排序要将排序的数据分割成独立的两部分,
其中一部分数据都比另一部分所有数据要小。
然后再按此方法对两部分数据分别进行快速排序,
整个排序过程可以递归进行,以此将整个数据变成有序数据。
## 这里采用分区指示器的方式实现
def QuickSort(nums:list) -> list :
return sort(nums,0,len(nums)-1)
def sort(array:list,start,end):
if len(array) < 1 or start < 0 or end > len(array):
return None
zoneIndex = partition(array,start,end)
if zoneIndex > start:
sort(array,start,zoneIndex-1)
if zoneIndex < end:
sort(array,zoneIndex+1,end)
return array
def partition(array:list,start:int ,end:int ): ## 关键是如何实现将大的数排在一侧,小的排在另一侧
if start == end :
return start
pivot = start ## 本来是随机选定一个基准数,这里我只选择第一个数为基准数
zoneIndex = start - 1 ## 分区指示器放在开始的前一个位置
swap(array,pivot,end) ## 将基准数放到最后去,然后其它元素都和最后一个数比较即可
for i in range(start,end+1):
if array[i] <= a[end]: ## 如果遍历指示器元素比基准数小,分区指示器+1,否则不操作
zoneIndex += 1
if i > zoneIndex: ## 如果遍历指示器大于分区指示器,二者指向的元素交换位置
swap(array,i,zoneIndex)
return zoneIndex
def swap(array: list,i: int,j: int):
temp = array[i]
array[i] = array[j]
array[j] = temp
希尔排序
这是一种基于插入排序的快速排序算法。简单插入对于大规模乱序数组很慢,因为元素只能一点一点地从数组的一端移动到另一端。
希尔排序为了加快速度简单地改进了插入排序,也称为缩小增量排序,同时该算法是冲破
O
(
n
2
)
O(n^2)
O(n2)的第一批算法之一。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法;然后缩小增量继续分组,随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分为一组,再次排序完成整个数组的排序。这个不断缩小的增量,就构成了一个增量序列。
从理论上说,只要一个数组时递减的,并且最后一个值是1,都可以作为增量序列使用。但目前从数学上来说,无法证明哪个序列是“最好的”,即找不到一组序列使希尔排序时间复杂度最优。
常用的希尔序列:
希尔增量序列:
N
2
,
N
2
2
,
.
.
.
,
1
\frac{N}{2},\frac{\frac{N}{2}}{2},...,1
2N,22N,...,1,N为数组原始长度,这是最常用的序列,但不是最好的。
Hibbard序列:
2
k-1
,
.
.
.
,
3
,
1
2\raisebox{0.5em}{k-1},...,3,1
2k-1,...,3,1
这个序列就像是一个步长一样的东西,序列长度为10的话,
如果我们选择希尔增量序列,那么这个序列就是5,2,1,
那么如果下标从0开始,就先以步长为5,分为[0,5],[1,6],[2,7],[3,8],[4,9]这5组。
对每一组采用插入排序,先保证组内有序。
然后步长为2,分为[0,2,4,6,8],[1,3,5,7,9]这两组,
对这两组组内采用插入排序。
最后增量为1,步长为1的话,就是对整个数组采用插入排序。
这样整个数组就被排好序了。
## python3
def shellSort(nums:list) -> list:
lenth = len(nums)
currenValue = int(lenth/2)
gap = int(lenth/2) ## 增量序列
while gap > 0 :
for i in range(gap,lenth):
currenValue = nums[i]
preIndex = i-gap ## 组内已被排序数据的索引
## 在组内已被排过序数据中倒序寻找合适的位置,如果当前待排序数据比cur要小,就把cur后移
while preIndex >= 0 and nums[preIndex] > currenValue:
nums[preIndex + gap] = nums[preIndex]
preIndex -= gap
nums[preIndex + gap] = currenValue ## while之后就找到了合适的位置,应该进行插入
gap = int(gap/2)
return nums
归并排序
对于给定的一组数据,利用递归与分治技术将数据序列划分为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。
举个例子
原始数组:[35,63,48,9,86,24,53,72]
分为一半[35,63,48,9] , [86,24,53,72]
再分 [35,63] [48,9] , [86,24] [53,72]
数组已经够小了,可以直接排序
[63,35] [48,9] , [86,24] [72,53]
也可以不排序,直接分到一个元素为止,然后合并。
进行合并,在左边和右边的初始位置设置指针。
将两个指针指向的元素进行比较,将小的填入数组(应该专门设一个数组保存结果)
然后指针后移,直到左右数组都被遍历完。
合并 [63,48,35,9] [86,82,53,24]
再合并 [86,82,63,53,48,35,24,9]
## python3
def MergeSort(nums:list)->list:
if len(nums) < 2: ## 当列表只有一个元素时,停止递归
return nums
mid = int(len(nums)/2)
left = nums[:mid]
right = nums[mid:]
return merge(MergeSort(left),MergeSort(right)) ## 继续拆分并合并
def merge(left: list,right: list) -> list:
i,j, = 0,0
result = [None]*(len(left)+len(right))
for index in range(0,len(result)):
if i >= len(left): #左边先被遍历完
result[index] = right[j]
j += 1
elif j >= len(right): # 右边先被遍历完
result[index] = left[i]
i += 1
elif left[i] > right[j]:
result[index] = right[j]
j += 1
else:
result[index] = left[i]
i += 1
return result
并没有规定只能分为两部分,可以分成多个部分再合并,分为两个部分写起来很简单。
堆排序
许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或者不一定要一次就将他们排序,很多时候,我们每次只需要操作数据中最大或最小元素,那么就有一种基于二叉堆的数据结果可以提供支持。
所谓二叉堆,是一个完全的二叉树结构,同时满足堆的性质:即子结点的键值或索引总是小于或者大于它的父结点。在一个二叉堆中,根结点总是最大(或最小)结点,这样的堆我们称之为最大(小)堆。
堆排序算法就是抓住了这一特点,每次都取堆顶的元素,然后将剩余的元素重新调整为最大(最小)堆,最终得到排序的序列。
完全二叉树
推论1:对于位置为k的结点,左子结点=2k+1,右子结点=2(k+1)
推论2:最后一个非叶子结点的位置为
N
2
−
1
\frac{N}{2}-1
2N−1,N为数组长度
## python3
class HeapSort:
lenth = None
## swap用于交换组内两个元素
def swap(self,array:list,i:int,j:int):
temp = array[i]
array[i] = array[j]
array[j] = temp
def sortArray(self,nums:list) -> list:
self.lenth = len(nums)
if self.lenth < 1:
return nums
self.buildMaxHeap(nums) ## 构建一个最大堆
while self.lenth > 0:
swap(nums,0,self.lenth-1)
self.lenth -= 1
self.adjustHeap(nums,0)
return nums
## buildMaxHeap用于建立最大堆
def buildMaxHeap(self,array:list):
#从最后一个非叶结点开始向上构造最大堆
for i in range(int(self.lenth/2)-1,-1,-1):
self.adjustHeap(array,i)
def adjustHeap(self,array:list,i:int):
maxIndex = i
left = 2*i +1
right = 2 * (i+1)
#如果有左子树,且左子树大于父结点,则将最大指针指向左子树
if left < self.lenth and array[left] > array[maxIndex]:
maxIndex = left
## 如果有右子树,并且右子树大于父结点且大于左子树,则把最大指针指向右子树
if right < self.lenth and array[right] > array[left] and array[right] > array[maxIndex]:
maxIndex = right
## 如果父结点不是最大结点,则将父结点与最大值交换,并且递归调整与父结点的位置
if maxIndex != i:
swap(array,maxIndex,i)
self.adjustHeap(array,maxIndex)
计数排序
计数排序是一种排序时不比较元素大小的排序算法。
计数排序对一定范围内的整数排序时速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限定对于整数排序、并且待排序元素值分布比较连续、跨度小的情况。
如果一个数组里所有元素都是整数而且都在0~k以内。那对于数组里每一个元素来说,如果知道数组里有多少项小于或等于该元素,就能准确地给出该元素在排序后的数组的位置。
原始数组:[2,5,3,0,2,3,0,3]
对于这个数组来说,元素5之前有8个元素小于或等于5,因此排序后5所在的位置肯定是7。
只要构造一个(5+1)大小的数组,里面存下所有对应A中每个元素之前的元素个数,就能在线性时间内完成排序。
数组中最大值为5,所以构造一个(5+1)= 6 的数组,所有元素初始值为0,遍历整个数组,将原始数组中每个元素值转化为计数数组的下标,并将计数数组中下标对应元素值大小+1.
遍历计数数组,根据计数数组直接对原始数组进行更改。
实际应用中,我们要同时找出数组的max和mi你,以便节省空间。
如果待排序数组的元素值之间跨度很大,如[9999,1,2],计数排序将浪费大量空间。所以计数排序适于用在待排序元素值分布比较连续,跨度小的情况
## python3
def CountingSort(nums:list)->list:
if len(nums) == 0:
return nums
min,max = nums[0],nums[0]
## 先找出最大、最小值
for i in range(len(nums)):
if nums[i] < min:
min = nums[i]
if nums[i] > max:
max = nums[i]
## 计数数组初始化为0
counterArray = [0] * (max - min + 1)
for i in range(len(nums)):
counterArray[nums[i] - min] += 1
index,i = 0,0
## 重新写入原始数组
while index < len(nums):
if counterArray[i] != 0:
nums[index] = i + min
counterArray[i] -= 1
index += 1
else:
i += 1
return nums
桶排序
桶排序是计数排序的升级版
桶排序的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶在分别排序(可以使用其他的排序算法或者递归调用桶排序)。
原始数组:[35,23,48,9,16,24,5,11,32,17]
可以建立5个桶,每个桶顺序依次是[0,10) [10,20) [20,30) [30,40) [40,50) ,注意是左闭右开。
将桶内排好序,再依次从桶中把元素取出则能将原始数组排好序。
def BucketSort(array:list,bucketCap:int) -> list:
# bucketCap:桶的容量
if array == None or len(array) < 2:
return array
maxx,minn = max(array),min(array)
## 设置桶的数量
bucketCount = int( (maxx - minn) / bucketCap) + 1
bucketArr = [[] for i in range(bucketCount)]
result = []
for i in range(len(array)):
bucketArr[int ((array[i] - minn) / bucketCap)].append(array[i])
for i in range(bucketCount):
if bucketCap == 1:
for j in range(len(bucketArr[i])):
result.append(bucketArr[i][j])
else:
if bucketCount == 1:
bucketCap -= 1
temp = BucketSort(bucketArr[i],bucketCap)
for j in range(len(temp)):
result.append(temp[j])
return result
基数排序
常见的数据元素一般是由若干位组成的,比如字符串由若干字符组成,整数由若干位0~9的数字组成。基数排序按照从右往左的顺序依次将每一位都当作一次关键字,然后按照该关键字对数组的元素入桶,每一轮入桶都基于上一轮入桶的结果;完成所有位入桶后,整个数组就达到了有序状态。
基数排序也是一种无序比较的算法。
举个例子
def RadixSort(nums:list)->list:
if nums == None or len(nums) < 2:
return nums
maxx = max(nums)
## 先找出最大数的位数,这决定了要进行多少轮排序
maxDigit = 0
while maxx != 0:
maxx /= 10
maxDigit += 1
mod,div = 10,1
bucketList = [[] for i in range(10)]
for i in range(maxDigit):
# 入桶
for j in range(len(nums)):
num = int((nums[j] % mod) / div)
bucketList[num].append(nums[j])
# 写回原始数组,并清除桶
index = 0
for j in range(len(bucketList)):
for k in range(len(bucketList[j])):
nums[index] = bucketList[j][k]
index += 1
bucketList[j].clear()
mod *= 10
div *= 10
return nums
基数排序,计数排序和桶排序都利用了桶的概念,但在桶的使用上有明显的差异
基数排序:根据键值的每位数字来分配桶
计数排序:每个桶只存储单一键值
桶排序:每个桶存储一定范围的数值
总结
平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 | 比较算法 | |
---|---|---|---|---|---|---|
冒 泡 排 序 冒泡排序 冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳 定 稳定 稳定 | 是 是 是 |
选 择 排 序 选择排序 选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不 稳 定 不稳定 不稳定 | 是 是 是 |
插 入 排 序 插入排序 插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳 定 稳定 稳定 | 是 是 是 |
希 尔 排 序 希尔排序 希尔排序 | O ( n l o g n ) ∼ O ( n 2 ) O(nlogn)\sim O(n^2) O(nlogn)∼O(n2) | O ( n 1.3 ) O(n\raisebox{0.25em}{1.3}) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不 稳 定 不稳定 不稳定 | 是 是 是 |
归 并 排 序 归并排序 归并排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n ) O(n) O(n) | 稳 定 稳定 稳定 | 是 是 是 |
快 速 排 序 快速排序 快速排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( l o g n ) O(logn) O(logn) | 不 稳 定 不稳定 不稳定 | 是 |
堆 排 序 堆排序 堆排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( 1 ) O(1) O(1) | 不 稳 定 不稳定 不稳定 | 是 是 是 |
计 数 排 序 计数排序 计数排序 | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | O ( k ) O(k) O(k) | 稳 定 稳定 稳定 | 否 否 否 |
桶 排 序 桶排序 桶排序 | O ( n + c ) O(n+c) O(n+c) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n + k ) O(n+k) O(n+k) | 稳 定 稳定 稳定 | 否 否 否 |
基 数 排 序 基数排序 基数排序 | O ( n × k ) O(n \times k) O(n×k) | O ( n × k ) O(n \times k) O(n×k) | O ( n × k ) O(n \times k) O(n×k) | O ( n + k ) O(n+k) O(n+k) | 稳 定 稳定 稳定 | 否 否 否 |