Selection Sort
选择排序,即依次在待排序列表中找出最小者,放置到待排序列表最左端。初始时,待排序列表为整个列表。
def selection_sort(lst):
def _min(start):
value = lst[start]
index = start
for i in range(start + 1, len(lst)):
if lst[i] < value:
value = lst[i]
index = i
return index
for i in range(len(lst) - 1):
min_index = _min(i)2211
if min_index != i:
lst[i], lst[min_index] = lst[min_index], lst[i]
return lst
时间复杂度,O(n2)
Insertion Sort
插入排序,意为将新元素插入到已排序元素中的合适位置。初始时,已排序元素为第一个元素,然后从第二个元素开始将其依次插入到合适位置。插入时,从右往左依次与已排序元素进行比较,若待插入元素比已排序元素小,则将已排序元素向后移动一位,直到找到合适位置。
def insertion_sort(lst):
for i in range(1, len(lst)):
ptr = i - 1
temp = lst[i]
while ptr >= 0 and temp < lst[ptr]:
lst[ptr + 1] = lst[ptr]
ptr -= 1
lst[ptr + 1] = temp
return lst
最好情况,列表已从小到大排序,时间复杂度O(n);最坏情况,列表已从大到小排序,时间复杂度O(n2)
Shell Sort
作为插入排序的改进版,希尔排序通过将列表分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n2)的排序(冒泡排序或插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
每次排序的分区大小由步长值gap
决定,有多种步长值可供选择。下面选择length/2k
def shell_sort(lst):
gap = len(lst) // 2
while gap >= 1:
for start in range(gap):
for i in range(start, len(lst), gap):
# insertion
ptr = i - gap
temp = lst[i]
while ptr >= 0 and temp < lst[ptr]:
lst[ptr + gap] = lst[ptr]
ptr -= gap
lst[ptr + gap] = temp
gap //= 2
return lst
时间复杂度根据gap
序列而定。
Bubble Sort
冒泡排序,即不断地通过相邻两元素交换,将较大或较小者交换到列表一端。若在某趟排序中,并没有发生元素交换,则说明所有元素已经排序完成。
def bubble_sort(lst):
for end in range(len(lst) - 1, 0, -1):
swapped = False
for i in range(end):
if lst[i] > lst[i + 1]:
lst[i], lst[i + 1] = lst[i + 1], lst[i]
swapped = True
if not swapped:
break
return lst
最好情况,列表已从小到大排序,时间复杂度O(n);最坏情况,要进行n-1次遍历交换操作,时间复杂度O(n2)
Quick Sort
快速排序,即从列表中选一个元素作为中心点,将其余元素中小于该值者归为一类,大于或等于该值者归为一类,然后分别对上述两类元素集进行快速排序。
def quick_sort(lst):
def sort(left, right):
if left < right:
p_left = left
p_right = right + 1
while p_left < p_right:
p_left += 1
p_right -= 1
while p_left <= right and lst[p_left] < lst[left]:
p_left += 1
while p_right >= left and lst[p_right] > lst[left]:
p_right -= 1
if p_left < p_right:
lst[p_left], lst[p_right] = lst[p_right], lst[p_left]
lst[left], lst[p_right] = lst[p_right], lst[left]
sort(left, p_right - 1)
sort(p_right + 1, right)
sort(0, len(lst) - 1)
return lst
最坏情况下,列表已排序,时间复杂度O(n2);平均情况,时间复杂度O(nlog2n)
Merge Sort
归并排序,将有n个元素的列表视为由n个子列表组成,然后两两合并子列表,排序之,然后继续合并合并后的子列表,直到合并成一个列表。
def merge_sort(lst):
last = len(lst) - 1
def merge(left, right, count):
temp = []
end = right + count - 1 if right + count - 1 <= last else last
p_left = left
p_right = right
while p_left < right and p_right <= end:
if lst[p_left] <= lst[p_right]:
temp.append(lst[p_left])
p_left += 1
else:
temp.append(lst[p_right])
p_right += 1
if p_left <= right - 1:
temp += lst[p_left:right]
if p_right <= end:
temp += lst[p_right: end + 1]
for i in range(len(temp)):
lst[left + i] = temp[i]
count = 1
while count < len(lst):
left = 0
right = count
while right <= last:
merge(left, right, count)
left += count * 2
right += count * 2
count *= 2
return lst
count
的取值不超过[O(log2n)],因此最多合并O(log2n)次,每次合并都要遍历所有n元素,故时间复杂度为O(nlog2n)
Heap Sort
堆有以下性质:
- 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
- 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆排序使用二叉堆,二叉堆可以用一维数组表示:
- 父节点i的左子节点在位置(2*i+1);
- 父节点i的右子节点在位置(2*i+2);
- 子节点i的父节点在位置floor((i-1)/2);
堆排序算法如下:
- 构建最大堆(先将初始序列视为完全二叉树,然后将其调整为最大堆)
- 将堆顶元素与堆底最右元素交换,相当于取出最大者。并将堆底最后元素从堆中移除
- 交换后,当前二叉树可能不满足堆的性质。因此,将堆顶元素下移,找到其合适位置,使之再次满足堆的性质
- 不断地交换堆底与堆顶元素,直至堆中只剩下一个元素
def heap_sort(lst):
def heapify():
for start in range(((len(lst) - 1) // 2), -1, -1):
shift_down(start, len(lst) - 1)
def shift_down(start, end):
if start * 2 + 1 > end:
return
left_index = start * 2 + 1
right_index = start * 2 + 2
parent = lst[start]
left = lst[left_index]
right = lst[right_index] if right_index <= end else left - 1
if parent < max(left, right):
if left > right:
lst[start], lst[left_index] = lst[left_index], lst[start]
shift_down(left_index, end)
else:
lst[start], lst[right_index] = lst[right_index], lst[start]
shift_down(right_index, end)
heapify()
for end in range(len(lst) - 1, 0, -1):
lst[0], lst[end] = lst[end], lst[0]
shift_down(0, end - 1)
return lst
shift_down
执行时间不超过O(log2n),故时间复杂度为O(nlog2n)
稳定性分析
稳定性指的是,对列表中任意相同的两个元素,排序后,二者的相对次序不变。
- Selection Sort: 不稳定。选择最小元素后会将其交换到未排序区首端,此时可能破坏相对次序
- Insertion Sort: 稳定。依次插入时不会破坏相对次序
- Shell Sort: 不稳定。 会将相同元素分到不同组,排序后打乱相对次序。
- Bubble Sort: 稳定。交换过程中遇到相同元素不会交换
- Quick Sort: 不稳定。交换元素时可能破坏稳定性
- Merge Sort: 稳定。合并子序列时,若左右两侧元素相等,会优先选择左侧元素
- Heap Sort: 不稳定。堆顶元素与堆底元素交换时可能会破坏稳定性
为啥要稳定呢?因为当分别对元素的某个关键字进行排序时,就不能随便打乱相对次序了。比如对人名排序(“姓+名”),“Smith Bob”、“Smith Jone”先按last name排,再按first name排。
源码见https://gist.github.com/YieldNull/cbe70dc07f0c53c73b2478ec6b877882