Python中常用的排序算法

冒泡排序:

冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

冒泡排序算法的运作如下:

  • 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。

  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

  • 针对所有的元素重复以上的步骤,除了最后一个。

  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
    [8, 9, 7, 6, 5, 4, 3, 2, 1]

    第1趟 遍历前: [8,9,7,6,5,4,3,2,1] => [8,7,6,5,4,3,2,1, 9] 遍历后
    第2趟 遍历前: [8,7,6,5,4,3,2,1, 9] => [7,6,5,4,3,2,1, 8,9] 遍历后
    第3趟 遍历前: [7,6,5,4,3,2,1, 8,9] => [6,5,4,3,2,1, 7,8,9] 遍历后
    第4趟 遍历前: [6,5,4,3,2,1, 7,8,9] => [5,4,3,2,1, 6,7,8,9] 遍历后
    第5趟 遍历前: [5,4,3,2,1, 6,7,8,9] => [4,3,2,1, 5,6,7,8,9] 遍历后
    第6趟 遍历前: [4,3,2,1, 5,6,7,8,9] => [3,2,1, 4,5,6,7,8,9] 遍历后
    第7趟 遍历前: [3,2,1, 4,5,6,7,8,9] => [2,1, 3,4,5,6,7,8,9] 遍历后
    第8趟 遍历前: [2,1, 3,4,5,6,7,8,9] => [1, 2,3,4,5,6,7,8,9] 遍历后


l = [8,  9, 7, 6, 5, 4, 3, 2, 1]
for i in range(len(l)- 1):  # i =0,1,2,3,4,5,6,7  共8趟
    for j in range(len(l)-1-i):  # j 遍历剩下未排好序的数
        if l[j] > l[j+1]:
            l[j], l[j+1] = l[j+1], l[j]
print(l)
时间复杂度
  • 最优时间复杂度:O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
  • 最坏时间复杂度:O(n2)
  • 稳定性:稳定

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

            [8, 9, 7, 6, 5, 4, 3, 2, 1]

第1趟 遍历前: [8,9,7,6,5,4,3,2,1]   => [1, 9,7,6,5,4,3,2,8]  遍历后
第2趟 遍历前: [1, 9,7,6,5,4,3,2,8]  => [1,2, 7,6,5,4,3,9,8]  遍历后
第3趟 遍历前: [1,2, 7,6,5,4,3,9,8]  => [1,2,3, 6,5,4,7,9,8]  遍历后
第4趟 遍历前: [1,2,3, 6,5,4,7,9,8]  => [1,2,3,4, 5,6,7,9,8]  遍历后
第5趟 遍历前: [1,2,3,4, 5,6,7,9,8]  => [1,2,3,4,5, 6,7,9,8]  遍历后
第6趟 遍历前: [1,2,3,4,5, 6,7,9,8]  => [1,2,3,4,5,6, 7,9,8]  遍历后
第7趟 遍历前: [1,2,3,4,5,6, 7,9,8]  => [1,2,3,4,5,6,7, 9,8]  遍历后
第8趟 遍历前: [1,2,3,4,5,6,7, 9,8]  => [1,2,3,4,5,6,7,8, 9]  遍历后


l = [8,  9, 7, 6, 5, 4, 3, 2, 1]
# 方法一
for i in range(len(l)-1):  # i =0,1,2,3,4,5,6,7  共8趟,每趟找最小数的下标
        # 找到剩下未排序的数中的最小下标
    min_index = i
    for j in range(i, len(l)):
        if l[j] < l[min_index]:
            min_index = j

    # 用剩下数中的最小数和第i个(剩下数中的第一个)元素交换
    l[i], l[min_index] = l[min_index], l[i]
print(l)

# 方法二
for i in range(len(l)-1):
	for j in range(i+1, len(l)-1):
		if l[i]> l[j]:
			l[i], l[j] = l[j], l[i]
print(l)
			
时间复杂度
  • 最优时间复杂度:O(n2)
  • 最坏时间复杂度:O(n2)
  • 稳定性:不稳定(考虑升序每次选择最大的情况)

插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
在这里插入图片描述

# 从第二个位置,即下标为1的元素开始向前插入
for i in range(1, len(l)):
	# 从第i个元素开始向前比较,如果小于前一个元素,交换位置
	for j in range(i, 0, -1):
		if l[j] < l[j-1]:
			l[j], l[j-1] = l[j-1], l[j]
print(l)

时间复杂度
  • 最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
  • 最坏时间复杂度:O(n2)
  • 稳定性:稳定

快速排序

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

步骤为:

  • 从数列中挑出一个元素,称为"基准"(pivot),
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

[2, 4, 8, 5, 6, 7, 3, 0, 9, 1]
[ ] [ ] [ ]

方法一(简单易懂, 但是时间复杂度不是最优):

def quick_sort(l):
    # 临界值
    if len(l) < 2:
        return l
    # 先取一个中间值或者随机弹出一个
    mid = l.pop(len(l)//2)   # mid = l.pop()

    left = []  # 存放大于m的数  left = [i for i in l: if i<=mid)
    right = []  # 存放大于m的数 right = [i for i in l: if i>mid)
    for n in l:
        if n<mid:
            left.append(n)
        else:
            right.append(n)
    # print('left:', left)
    # print('right:', right)
    # 递归
    return quick_sort(left) + [mid] +quick_sort(right)

ll = [2, 4, 8, 5, 6, 7, 3, 0, 9, 1]
print(quick_sort(ll))

方法二(时间复杂度最优):

def quick_sort(l, start=0, end=len(l)-1):
	# 递归的退出条件
	if start >= end:
		return
	# 定义两个指针,一个指针指向第一个元素,另一个指针指向最后一个元素
	pre = start
	next = end
	# 设置基准元素
	mid = l[start]
	
	# 如果pre指针和next重合时跳出循环并把mid赋值给pre或者next
	while pre < next:
		# 如果next所指向的元素比基准元素大,就把next指针向前移动一位,否则跳出循环后把next所指向的元素赋值给pre
		while l[next] >= mid and pre < next:
			next -= 1
		l[pre] = l[next]
		# 如果pre所指向的元素比基准元素小,就把pre指针向后移动一位,否则跳出循环后把pre所指向的元素赋值给next
		while l[pre] < mid and pre < next:
			pre += 1
		l[next] = l[pre]
	l[pre] = mid
	
	# 递归调用函数对基准值左边的数进行排序
	quick_sort(l,start,pre-1)
	# 递归调用函数度基准值右边的数进行排序
	quick_sort(l,pre+1,end)
quick_sort(l)
print(l)
时间复杂度
  • 最优时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(n2)
  • 稳定性:不稳定
    从一开始快速排序平均需要花费O(n log n)时间的描述并不明显。但是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用O(n)的时间。在使用结合(concatenation)的版本中,这项运算也是O(n)。

在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为一的数列前,我们只要作log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要O(n)的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有O(n)个调用,这些被归纳在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值