方便起见,一下所有代码与解释都默认升序排列。且存储在列表中。
分别实现了1.冒泡排序、2.选择排序、3.插入排序、4.希尔排序、5.归并排序、6.快速排序、7.堆排序
1.冒泡排序:
冒泡排序是一种很简单但是效率不高的方式,要多次遍历列表。
第一趟:两两比较,a[i] > a[i+1]时交换它们的位置,这样遍历完整个序列就能选出将最大值放在最后。如图。
第二趟:除去最大值,选出剩余序列中的最大值(原序列的第二大)放在倒数第二个位置。
…
第n-1趟
实现代码如下:
def bubbleSort(alist):
for i in range(len(alist)-1): #这一循环表示要走n-1趟
for j in range(1, len(alist)-i): #每一趟需要进行n-i-1次的两两对比
if alist[j] < alist[j-1]:
alist[j], alist[j - 1] = alist[j-1], alist[j]
return alist
第一趟需要比较n-1次,第二趟n-2次,第三趟n-3次….最后一趟1次。所以这是一个等差序列。一共需要 (n−1)2/2 ( n − 1 ) 2 / 2 次比较。当然交换的部分就根据原始列表来决定。所以冒泡排序的时间复杂度为O( n2 n 2 )
2.选择排序
选择排序可以看成是冒泡排序的一个改进,原理跟冒泡一样,也是每次只选出一个数放在其正确的位置上。但是相比冒泡而言不用一直做交换。每次遍历只做一个交换。
第一趟:选出最大的值放在最后
第二趟:选出第二大的值放在倒数第二处
…
代码实现:
def selectionSort(alist):
for i in range(len(alist) - 1):
positionOfMax = 0
for j in range(1, len(alist) - i):
if alist[j] > alist[positionOfMax]:
positionOfMax = j
alist[positionOfMax], alist[len(alist)-i-1] = alist[len(alist)-i-1], alist[positionOfMax]
return alist
选择排序的时间复杂度跟冒泡排序一样,也是O( n2 n 2 )
3.插入排序
插入排序有一个很形象的比喻:打扑克牌时,一张一张的摸牌,每摸一张就把它放在合适的位置,这就保证了手里的牌一直是有序的。插入排序也是这样,始终在列表的较低位置维护一个排好序的子列表。然后将每个新项“插入”回之前的子列表。
代码实现:
def insertSort(alist):
for i in range(1, len(alist)):
currentValue = alist[i]
position = i
while position > 0 and alist[position - 1] > currentValue:
alist[position] = alist[position - 1]
position -= 1
alist[position] = currentValue
同样插入排序的时间复杂度也是 O(n2) O ( n 2 )
4.希尔排序
shell排序(也称递减增量排序)算是插入排序的一个改进。通过将原始列表分解为多个较小的子列表进行插入排序。因为插入排序一次只能将数据移一位,希尔排序可以一次将数据移一大步。shell排序使用增量i(有时称为 gap),通过选择gap来创建子列表。
以上图为例:
第一步:设置增量为3,把原始列表分成了3个子列表,第一个是(54,17,44)第二个是(26,77,55)第三个(93,31, 20)
第二步:分别对这三个子列表进行插入排序。排序后的结果分别为(17,44,54)、(26,55,77)和(20,31,93)。实际上整个序列为(17,26,20,44, 55, 31, 54,77,93)
最后设置增量为1(即插入排序),对上一步的数据排序。就得到(17, 20, 26, 31, 44, 54, 55, 77, 93)
总结一下,希尔排序的关键在于怎么选取那个增量gap,增量要越来越小,也就是子列表越来越大。最后使用一次插入排序。因为如果列表本身已经排好序,或者大部分都排好了,那么插入排序的效率还是很高的。
代码实现:
设定gap的值由n/2–>n/4–>…–>1
相当于是第一次有n/2个子列表,每个子列表最多比较两个数(边界可能只有一个数)。第二次有n/4个子列表,每个子列表最多比较4个数。依次类推。。。
def shellSort(alist):
subListCount = len(alist)//2 #gap(子列表的个数)先为n//2
while subListCount > 0:
for i in range(subListCount): #分别比较每一个子列表
gapInsertSort(alist, i, subListCount) #调用插入排序,同时将子列表的第一个索引和gap传进去。
subListCount //= 2 #排完一个循环以后,将子列表个数缩减一半,子列表的扩大一倍。
def gapInsertSort(alist, start, gap): #实际是一个插入排序
for i in range(start+gap, len(alist), gap): #待排序的列表为[start, start+gap, start+2*gap...]
currentValue = alist[i] #要将alist[i]放置到正确的位置
position = i
while position >= gap and currentValue < alist[position - gap]: #待排数字是小于position-gap处的数时,那么position-gap就应该放到position处。一直到待排数字是大于当前比较数字,说明待排数已经到了它正确的位置上了
alist[i] = alist[position - gap]
position = position - gap
alist[position] = currentValue #最后将待排数放到它正确的位置上
希尔排序的时间复杂度跟gap的选择有关。
5.归并排序
归并算法是一个比较高效的算法,采用了分而治之的思想。用了递归的思想,不断地将列表一分为二,直到不能分为止,然后进行排序。
上图为拆分的过程:
1.将原列表分为两部分。
2.将分好的子列表,再各分成两部分。
…一直到不能分为止
然后是合并的过程:
首先排最小的子列表,排好以后进行合并。合并的过程是根据拆分的顺序,反着合并。最终得到排好的列表。
代码如下:
用递归的方式:
def mergeSort(alist):
if len(alist) > 1:
midPosition = len(alist)//2 #切分
left = alist[:midPosition]
right = alist[midPosition:]
mergeSort(left)
mergeSort(right)
i = 0 #左列表的索引
j = 0 #右列表的索引
k = 0 #合并列表的索引
while i < len(left) and j < len(right): #这一部分就相当于合并操作了
if left[i] < right[j]: #在right和left中选出最小的放在alist上。因为right和left都是排好序的,因此只需要在他们各自最低位中找。
alist[k] = left[i]
i += 1
elif left[i] > right[j]:
alist[k] = right[j]
j += 1
k += 1
while i < len(left): #left中还有剩余
alist[k] = left[i]
i += 1
k += 1
while j < len(right): #right中还有剩余
alist[k] = right[j]
j += 1
k += 1
return alist
分析复杂度:
第一步是拆分,假设列表长度为n,第一次分了2个,第二次分了
22
2
2
个。。。第i次分了
2i
2
i
个,所以
2i
2
i
= n,就能得出i =
log2n
l
o
g
2
n
次。大小为 n 的列表的合并需要 n 个操作,也就是说每一层需要n次。一共拆了
log2n
l
o
g
2
n
次,也就是有
log2n
l
o
g
2
n
层,所以归并排序是一种
O(nlog2n)
O
(
n
l
o
g
2
n
)
算法。
6.快速排序
快速排序也使用了分而治之的思想,但不同的是它不需要占用额外的存储,也就是不用像归并那样分成不同的块。
第一步:在列表中选择一个值,称为枢纽值。假设将列表第一项54选为第一个枢纽值,如图
第二步:将列表分为两部分,其中<54的在左边,>=54移到右边。但是怎么移到很关键,
- 1.定义左右两个位置标记。如图leftmark和rightmark分别指向除枢纽值以外的两个端点
- 2.然后向右移动leftmark,直到leftmark所指的值大于等于枢纽值’54’。如图中’93’就是左位置找到的第一个大于’54’的值
- 3.定位到rightmark,开始向左移动rightmark,直到rightmark所指的值小于枢纽值’54’,如图中’20’
- 4.交换‘93’和‘20’,这样就把小的换到左边,大的换到右边了
第三步:重复第二步,直到leftmark>rightmark,这时就交换rightmark的值和枢纽值‘54’。这样就以‘54’位分隔点将列表分成了两个部分了。
第四步:用同样的方式对两个子列表排序。直到子列表的长度<=1。
代码:
def quickSort(alist):
quickSortCore(alist, 0, len(alist) - 1)
def quickSortCore(alist, first, last):
if first < last:
splitPoint = partition(alist, first, last) #对alist的first-last这一段排序,得到分割点位置
quickSortCore(alist, first, splitPoint-1)
quickSortCore(alist, splitPoint + 1, last)
def partition(alist, first, last):
pivotValue = alist[first] #默认以子列表的第一个数为枢纽值
leftMark = first + 1 #左端点
rightMark = last #右端点
while leftMark < rightMark:
while alist[leftMark] < pivotValue:
leftMark += 1
while alist[rightMark] > pivotValue:
rightMark -= 1
if leftMark < rightMark:
alist[leftMark], alist[rightMark] = alist[rightMark], alist[leftMark]
rightMark -= 1
leftMark += 1
alist[rightMark], alist[first] = alist[first], alist[rightMark]
return rightMark
7.堆排序
堆排序需要理解堆的含义,它的一些函数,例如下沉、上浮、创建堆等,这些可以查看链接
具体的实现过程就是,给定一个列表,首先将它转为为堆。因为找的图为降序,所以用以降序来讲。如果是降序的话,那么需要转换为最小堆。
然后依次进行删除根节点(最小的值)操作,删除当前堆的根节点后,用当前最末的节点代替根节点,进行下沉操作,这样就又能将最小的节点放置到根节点。相当于依次将最小的节点放在后面,这样的话,最后的列表将是一个降序排列。
如上图所示:
这是一个最小堆,根节点为最小值,
1. 取出最小值5,将最末的27放置到根节点,形成新的一个二叉树
2. 对27进行下沉操作,一直到形成一个新的最小堆,这时根节点处又是最小值9.
3. 依次进行步骤2的操作。直到取出所有点。
代码实现:
def heapSort(alist):
def maxChild(i, end):
if i * 2 + 1 > len(alist) or i*2+1>end:
return 2 * i
return 2 * i if (alist[2 * i] < alist[2 * i + 1]) else 2 * i + 1
def percDown(start, end):
while start * 2 <= end:
mc = maxChild(start, end)
if alist[start] > alist[mc]:
alist[start], alist[mc] = alist[mc], alist[start]
start = mc
def buildHeap(alist):
i = len(alist) // 2
alist.insert(0, 0)
while i > 0:
percDown(i, len(alist) - 1)
i -= 1
#程序从这里开始运行
buildHeap(alist) #将alist构建成一个最小堆
for i in range(len(alist)-1, 1, -1): #依次取出最小的值放在后面。再调整二叉树为一个新的最小堆
alist[i], alist[1] = alist[1], alist[i]
percDown(1, i-1)
return alist[1:]
print(heapSort([6, 5, 9, 7, 3, 19, 0, 7, 2]))
结果为
以上代码解释:
1. 先构建一个最小堆。
2. 那么第一个节点,一定是最小的。所以把第一个节点和最后一个节点交换位置。
3. 交换完以后,应该固定住最后一个节点,因为它已经放在了正确的位置上了。然后再调整二叉树,使其成为一个新的最小堆。调整的方式是,把第一个节点当做新加入的,然后进行下沉。
4. 再取出新的二叉堆的根节点,与倒数第二个节点交换。。。
5. 重复进行以上步骤,使得最后只剩下一个点为止。
8 基数排序
基数排序用于整数排序。原理是将整数按位数切割成不同的数字,然后按位比较。
实现过程:将所有待比较的数统一为相同的位数长度,位数短的前面补零,然后开始依次按位比较。基数排序分为LSD(Least significant digital)和MSD(Most significant digital)。LSD从键值最右(最低位)开始,MSD从键值最左边(最高位)开始。
例如:给321,109,584,123,052,063,765,814排序
用LSD方式:这些数字一共有3位,也就是说会排3次
第一次:排个位。
Q[0]:
Q[1]:321
Q[2]:052
Q[3]:123, 063
Q[4]:584, 814
Q[5]:765
Q[6]:
Q[7]:
Q[8]:
Q[9]:109
结果:321,052,123,063,584,814,765
第二次:排十位
Q[0]:
Q[1]:814
Q[2]:321,123
Q[3]:
Q[4]:
Q[5]:052
Q[6]:063,765
Q[7]:
Q[8]:584
Q[9]:
结果:814,321,123,052,063,765,584
第三次:排百位
Q[0]:052,063
Q[1]:123
Q[2]:
Q[3]:321
Q[4]:
Q[5]:584
Q[6]:
Q[7]:765
Q[8]:814
Q[9]:
结果:052,063,123,321,584,765,814
通常把Q[0]….Q[9]称为桶,所以这种排序方法也称为桶排序。
基数排序的时间复杂度是O(k*n),其中n是排序元素个数,k是数字位数。
import math
def RadixSort(alist, radix = 10):
if max(alist) == 0:
K = 1
else:
K = int(math.ceil(math.log(max(alist), radix))) #位数
for i in range(0, K):
bucket = [[] for i in range(radix)] #建立radix个桶
for val in alist:
bucket[val % (radix**(i+1)) // (radix**i)].append(val) #获得val的第i位数字,并将val放到对应的桶
del alist[:]
for each in bucket:
alist.extend(each) #合并桶
return alist
print(RadixSort([321,109 ,584,123, 52, 63,765,814]))
稳定性
定义:相同大小的元素,在排序前后的相对位置不变,那么这种排序算法就是稳定的。
总结
排序方法时间复杂度和空间复杂度以及稳定性总结: