1. 冒泡排序
# coding=utf-8
'''冒泡排序
排序过程:
沉淀法(比大,大的下沉):(visualgo上面的动态图就是沉淀法)
第 1 次从 头 开始比较前后两个数字的大小,最大的数沉淀在第 n 位置,比较n-1次;
第 2 次从 头 开始比较前后两个数字的大小,次大的数沉淀在第 n-1 位置,比较n-2次;
...
第 n-1 次从头开始比较前后两个数字的大小,次小的数沉淀在第 2 位置,比较1次;
=== 此时,最小数和次小数经过第n-1次比较,最小数已经在第1个位置了 ===
第 n 次,该比较为无意义的,子循环条件也证明了:range(1,1),为空,没有比较操作了,比较0次.
冒泡法(比小,小的上浮):
第 1 次从 最后 开始比较前后两个数字的大小,最小的数浮在在第 1 位置,比较n-1次;
第 2 次从 最后 开始比较前后两个数字的大小,次小的数浮在在第 2 位置,比较n-2次;
...
第 n-1 次从 最后 开始比较前后两个数字的大小,次大数数浮在第 n-1 位置,比较1次;
=== 此时,最大数和次大数经过第n-1次比较,最大数已经被"浮"(沉淀)在第 n 个位置了 ===
第 n 次,该比较为无意义的,子循环条件也证明了:range(n-1,n-1),直接返回上一次的list,比较0次.
所以,外层循环条件可以写 range(n-1):循环总次数为: n-1 + n-2 + ... + 1
也可以写range(n):循环总次数为: n-1 + n-2 + ... + 1 + 0
平均时间复杂度为O(n^2);
最好情况(顺序)为O(n);
最坏情况(倒序)为O(n^2):n-1 + n-2 + ... + 1=n(n-1)/2
算法稳定.
改进的bubble_sort,因为冒泡排序的在当前子循环过程中,除了把当前最大数沉淀到最后,
在比较的过程中,会对当前遍历中的每个数进行比较,在到达最后的过程中,期间如果没有任何
交换操作,说明,当前遍历的已经是顺序的了,无需要在做后面的遍历了.现对每个遍历
加一个标志,什么时候整个遍历没有交换操作,则整个排序终止.
'''
list = [5, 8, 1, 4, 2, 7, 3, 6]
list_ascended = [1, 2, 3, 4]
list_descended = [5, 4, 3, 2]
def bubble_sort(list):
n = len(list)
for i in range(n): # 参数为 n 话,i从0到n-1,一共遍历 n 次
print(list)
flag = True
# 向下沉淀
# for j in range(1, n - i): # 以1为起点(从0开始比较),n-i为终点,向后遍历;
# if list[j - 1] > list[j]: # 每次子循环从最开始开始向下沉,沉淀到 n-i 为止
# list[j - 1], list[j] = list[j], list[j - 1]
# 向上冒泡
for j in range(n - 1, i, -1): # 以n-1为起点,i为终点,向前遍历(j取值范围为[n-1,i+1])
if list[j] < list[j - 1]: # 每次子循环从最底下向上冒,冒到 i 为止
list[j - 1], list[j] = list[j], list[j - 1]
flag = False # 一次交换都没有 flag 才能保持住原始的标志
if flag == True:
return list
return list
print(bubble_sort(list))
print('-----')
print(bubble_sort(list_ascended))
print('-----')
print(bubble_sort(list_descended))
# list = [5, 8, 1, 4]
# def bubble_sort(list):
# n = len(list)
# for i in range(n - 1):
# print(list)
# print('----')
# print("i\t%d"%i)
# # # 向下沉淀
# for j in range(1, n - i): # 每次子循环从最开始开始向下沉,沉淀到 n-i 为止
# if list[j - 1] > list[j]:
# list[j - 1], list[j] = list[j], list[j - 1]
# print("j\t%d"%j)
#
# # # 向上冒泡
# # for j in range(n - 1, i, -1): # 每次子循环从最底下向上冒,冒到 i 为止
# # if list[j] < list[j - 1]:
# # list[j - 1], list[j] = list[j], list[j - 1]
# return list
# print(bubble_sort(list))
2. 选择排序
# coding=utf-8
'''注意:“交换”是个比较耗时的操作!'''
'''选择排序
选择排序相比冒泡,大大减少了元素的交换次数.
其实我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。
我们可以通过设置一个变量min,然后每一次子循环中,仅存储较小元素的数组下标,
当前子循环结束之后,那这个变量存储的就是当前子循环最小元素的下标,此时再执行交换操作即可。
第 1 次遍历 n-1 个数,找到最小的数,然后与第 1 个数交换;
第 2 次遍历 n-2 个数,找到最小的数,然后与第 2 个数交换;
... ...
第 n-1 次遍历 1 个数,找到最小的数,然后与第 n-1 个数交换;
== 如果外层循环为 range(n-1),则没有第n次遍历;
如果外层为 range(n),则有第n次遍历,但是子循环条件为 range(n,n),仍为无效遍历.==
平均时间复杂度为 O(n^2),该算法不稳定.
'''
list = [5, 8, 1, 4, 2, 7, 3, 6]
# list_ascended = [1, 2, 3, 4]
# list_descended = [5, 4, 3, 2]
def select_sort(list):
n = len(list)
for i in range(n): # 参数为 n 和 n-1 都可以,本质上都是 n-1 次遍历
min = i # 记录当前最小数的序号(位置)
print(list)
for j in range(i + 1, n): # 以i+1为起点,0为终点,向后遍历
if list[j] < list[min]:
min = j # 记录当前最小数的序号
if min != i: # 将当前最小数放在子循环的开头
list[min], list[i] = list[i], list[min] # 每次只进行一次"交换"操作
return list
print(select_sort(list))
# print('-----')
# print(select_sort(list_ascended))
# print('-----')
# print(select_sort(list_descended))
3. 插入排序
# coding=utf-8
'''插入排序(Insertion Sort)
它的工作原理是通过构建有序序列,对于未排序数据,
在已排序序列中从后向前扫描,找到相应位置并插入。
步骤:
1.从第一个元素开始,该元素可以认为已经被排序
2.取出下一个元素,在已经排序的元素序列中从后向前扫描
3.如果该元素(已排序)大于新元素,将该元素移到下一位置
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置,并做插入到该位置后
5.重复步骤2~4
平均时间复杂度为 O(n^2),该算法稳定.
注:对于基本有序的数组,使用直接插入排序的效率是很高的.
'''
list = [5, 8, 1, 4, 2, 7, 3, 6]
# list_ascended = [1, 2, 3, 4]
# list_descended = [5, 4, 3, 2]
def insert_sort(list):
n = len(list)
for i in range(1, n):
print(list)
for j in range(i, 0, -1): # 以i为起点,0为终点,向前遍历
if list[j] < list[j - 1]:
list[j], list[j - 1] = list[j - 1], list[j]
else:
break
return list
print(insert_sort(list))
# print('-----')
# print(insert_sort(list_ascended))
# print('-----')
# print(insert_sort(list_descended))
4. 希尔排序
# coding=utf-8
'''希尔排序
希尔排序的实质就是分组插入排序,该方法又称*缩小增量(step)排序*,因DL.Shell于1959年提出而得名。
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
1.插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
2.但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
“比较”在希尔排序中是最主要的操作,而不是“交换”。用这样步长串行的希尔排序比插入排序和堆排序都要快,
甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。
平均时间复杂度为:O(nlogn)
希尔排序是非稳定排序算法。
'''
# list = [8,7,6,5,4,13,2,1]
list = [5, 8, 1, 4, 2, 7, 3, 6]
# list_ascended = [1, 2, 3, 4]
# list_descended = [5, 4, 3, 2]
def shell_sort(list):
step = len(list) // 2
while step > 0:
# print("step=%d"%step)
# print(list)
for i in range(step, len(list)):
# 类似插入排序, 当前值与指定步长之前的值比较, 符合条件则交换位置
while i >= step and list[i - step] > list[i]:
list[i], list[i - step] = list[i - step], list[i]
i -= step # 这里类似插入排序要逐步向前比较插入,每次跳过step的步长
# print(list)
step //= 2
return list
print(shell_sort(list))
5. 归并排序
# coding=utf-8
'''归并排序
归并排序(Merge sort,台湾译作:合并排序)是建立在归并操作上的一种有效的排序算法。
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用
步骤:
1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2.设定两个指针,最初位置分别为两个已经排序序列的起始位置
3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4.重复步骤3直到某一指针达到序列尾
5.将另一序列剩下的所有元素直接复制到合并序列尾
算法特点:
可以用一个例子来体会一下假如有这样一个数组{ 3,7,2,5,1,0,4,6 },
冒泡和选择排序的比较次数是25次。直接插入排序用了15次。
而归并排序的次数是相对稳定的,由我们上面提到的比较次数的计算方法,
我们的例子要合并4对长度为1的,2对长度为2的,和1对长度为4的。
归并排序的最多的比较次数为4 * 1 + 2 * 3 + 7 = 17次。
因为元素的随机性,直接插入排序也可能是相当悲剧的,归并排序在比较次数上的优势。
将数列分开成小数列一共要 logn 步,每一步合并有序数列(调用一次 merge),复杂度为O(n)
归并排序平均时间复杂度为O(nlogn),是一种稳定的算法.
'''
# list = [5, 8, 1, 4, 2, 7, 3, 6]
list = [8, 7, 6, 5, 4, 13, 2, 1]
# 合并数列
def merge(left, right):
list = []
i, j = 0, 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
list.append(left[i])
i += 1
else:
list.append(right[j])
j += 1
# 把比较后,较长的 list剩下来的部分接上去
list += left[i:]
list += right[j:]
return list
# 递归的方法从最小单元开始拆分,合并
def merge_sort(list):
if len(list) <= 1:
return list
num = len(list) // 2
left = merge_sort(list[:num]) # 递归调用,左半部分排好序
right = merge_sort(list[num:]) # 递归调用,右半部分排好序
# print('list:%s'%list)
# print('left:%s'%left)
# print('right:%s'%right)
# print('merge:%s'%merge(left, right))
# print('------')
return merge(left, right)
print(merge_sort(list))
6. 快速排序
# coding=utf-8
'''快速排序
它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
该方法的基本思想是:
1. 从数列中挑出一个元素,称为 “基准”(pivot).
2. 分区操作(partition):重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
快排的特点:
元素的移动效率高了,尽量让一次移动,为后面的操作服务.
——高效的排序算法对元素的移动效率都是比较高的。
时间复杂度:
第一个关键字正好是待排序的序列的中间值(比如1到8中的5),因此递归树是平衡的,
此时性能也比较好。此时时间复杂度是 O(nlogn)
在最坏状况下则需要Ο(n^2)次比较(如原始是咧为倒序倒序),此时的递归树是一棵严重偏斜的树。
稳定性:
由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。
参考:http://book.51cto.com/art/201108/287089.htm
'''
list = [5, 8, 1, 4, 2, 7, 3, 6]
# list = [8, 7, 6, 5, 4, 3, 2, 1]
def quick_sort(list, left, right):
if left >= right: # 当前分区排序的停止条件,返回的数列为 整个list(而不是分区中的部分数列的,但是发生变化的是分区中的部分数列)
return list # 当前分区间内操作完成后,小于基数的数都在基数左边,大于基数的数都在基数右边
start = left # start 记录当前排序分区的开始位置
end = right # end 记录当前分区排序的停止位置
key = list[start] # 每次分区开始后,以第一个数为比较的基数
# key = list[(start + end)//2]
while left < right:
# 右指针向前移动,直到找到比基数大的数,则跳出循环,并将该数赋值给左指针所指位置————
# 此时,右指针相当于在这个位置挖了个坑,随时等着左指针找一个小于基数的数来填上这个本来比基数大的数的位置.
while left < right and list[right] >= key:
right -= 1
list[left] = list[right]
# 左指针向后移动,直到找到一个大于基数的数,则跳出循环,并赋值给当前右指针所停留的位置————
# 即补上上次右指针在右边挖的坑,但赋值的同时,相当于左指针给自己当前位置又挖了个坑,再次等右指针的大数来填这个坑
while left < right and list[left] <= key:
left += 1
list[right] = list[left]
# 在左右相互填坑的游戏中,如果左右指针重合了——表示左指针左边的数都比基数小,右指针右边的数都比基数大
list[right] = key # 则最终这个坑由基数来填上,此时本轮填坑游戏结束,并在进去下个分区排序的流程前,通过 return返回list.
quick_sort(list, start, left - 1)
quick_sort(list, left + 1, end)
return list
print('排序结果为:')
print(quick_sort(list, 0, len(list) - 1))
7. 堆排序
# coding=utf-8
'''堆排序:
堆的概念:
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值
均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的.
堆排序的过程:
1.初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),
调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n个元素中最小的元素;
2.然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素;
3.重复1,2,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。
稳定性:
不稳定
时间复杂度:
nlogn
'''
# list = [9, 8, 7, 6, 5, 4, 3, 2, 1]
list = [50, 80, 10, 40, 20, 70, 30, 60]
# 堆排序,整个过程就是不断地从堆顶移除,调整最大数到堆顶,重复len(list)次
def heap_sort(list):
size = len(list)
build_heap(list, size)
for size_i in range(size)[::-1]:
list[0], list[size_i] = list[size_i], list[0] # 将放在堆顶的当前最大的数推出(放在最后一个当前遍历的最后一个位置,并将当前堆中的最后一个数放在堆顶.)
adjust_heap(list, 0, size_i) # 注意此时size=i,下一个迭代中整棵树截断了已经排好当前最大数,并且每次都是从堆顶开始进行下一次遍历
return list
# 创建堆:原始序列的堆,经过所有非叶子节点从后向前的调用adjust(递归方式,注意max的跟踪),保证调整后的堆为"大顶堆"
def build_heap(list, size):
for none_leaf in range(size // 2)[::-1]: # size//2 为最后一个非叶子节点的序号
adjust_heap(list, none_leaf, size)
# 调整堆,使得大小为size_i的树为"大顶堆"
def adjust_heap(list, parent, size):
lchild = 2 * parent + 1
rchild = 2 * parent + 2
if lchild < size: # 如果当前节点是非叶子节点(还有孩子),才有继续递归调整堆的必要
max = parent # max 记录了当前子树(三个节点)中最大的数的序号
if lchild < size and list[lchild] > list[max]:
max = lchild
if rchild < size and list[rchild] > list[max]:
max = rchild
if max != parent:
# 将当前最大数(孩子节点所在位置)和parent节点调换位置,以保证parent节点上的数比孩子节点都大
list[max], list[parent] = list[parent], list[max]
# 当前max经过上一步的调换,为上个parent节点的所在的数,如果还有孩子节点则继续adjust
adjust_heap(list, max, size)
print("排序之后:%s" %heap_sort(list))
8. 基数排序
# coding=utf-8
'''基数排序:
原理类似桶排序,这里总是需要10个桶,多次使用.
首先以个位数的值进行装桶,即个位数为1则放入1号桶,为9则放入9号桶,暂时忽视十位数
例如,待排序数组[62,14,59,88,16]
分配10个桶,桶编号为0-9,以个位数数字为桶编号依次入桶,变成下边这样
| 0 | 0 | 62 | 0 | 14 | 0 | 16 | 0 | 88 | 59 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |——桶编号
将桶里的数字顺序取出来,输出结果:[62,14,16,88,59]
再次入桶,不过这次以十位数的数字为准,进入相应的桶,变成下边这样:
由于前边做了个位数的排序,所以当十位数相等时,个位数字是由小到大的顺序入桶的,
就是说,入完桶还是有序:
| 0 | 14,16 | 0 | 0 | 0 | 59 | 62 | 0 | 88 | 0 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |——桶编号
稳定性:
基数排序法是属于稳定性的排序,在某些时候,基数排序法的效率高于其它的稳定性排序法。
效率:
其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数.
'''
import math
list = [14, 16, 59, 62, 188 ,276, 533, 64]
def radix_sort(list, radix=10): # 基数不仅可以以10为基,还可以以其他数为基
# k 记录了整数的最高位
k = int(math.ceil(math.log(max(list), radix))) # log 函数的默认底数为e,这里设置为10;ceil返回大于参数的最小数,为浮点型
bucket = [[] for _ in range(radix)]
for i in range(k):
for j in list:
i_int = int(j / (radix ** i) ) # 将小数点移动到当前比较的数位后
bucket[i_int % radix].append(j) # 再以基数取余,得到当前位上的数字;并装入对应的桶中
del list[:] # 清空列表
# 将这次桶装的数字按照当前顺序放入list,为下一次入桶准备
for s in bucket:
list += s
del s[:]
return list
print("排序之后:%s" % radix_sort(list)