几种典型的排序方法以及python实现

(1)冒泡排序

对数组N长度的每一对相邻元素比较,如果比较第一个比第二个大,即交换。
做完一轮交换,那么最大元素会落在数组的最后一个元素。
新的一轮是 除去最后一个元素后的数组N-1进行同样的处理。

def bubbleSort(arr):
    for i in range(1, len(arr)):
        for j in range(0, len(arr)-i):
            if arr[j] > arr[j+1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

但是这样的时间复杂度是O(N^2), 实际上是用这种方法经常会超出时间限制。

(2)选择排序

第一轮:
对于数组N,初始化第一个元素作为最小值,然后对后面的数进行比较,遇到遇到比第一个元素小的值,则更换最小值,继续往后比较,如果还有更小的,则更换最小值,一轮过后,就能找到数组的最小值了,然后跟第一个元素交换。
第二轮:
这个时候,第一个元素是最小值,现在除去第一个元素。对数组N-1重复前面的操作。

手写选择排序的代码:

def selectSort(Lis):
	#special cases
	if Lis is None: return Lis 
	if len(Lis) == 1: return Lis 
	
	L = len(Lis)
	for i in range(L-1):
		min_index = i 
		for j in range(i+1, L):if Lis[min_index] >  Lis[j]:
				min_index = j 
		if min_index != i:
			Lis[min_index], Lis[i] = Lis[i], Lis[min_index]
	return Lis 

同样,这样的时间复杂度是O(N^2), 实际上是用这种方法经常会超出时间限制。

(3)快速排序

把最后一个元素或者是第一个元素作为基准,对前N-1个数进行比较,如果比基准小的 放在左边。i作为位置来加一。
最后 交换i+1和基准的位置,返回 i+1 这个位置作为分界点。
不断循环左区间和右区间。

def partition(arr,low,high):
	i = low -1 
	pivot  = arr[high]
	for j in range(low,high):
		if arr[j]<= pivot:
			i +=1 
			arr[i],arr[j] = arr[j],arr[i]
	arr[i+1],arr[high] = arr[high],arr[i+1]
	return i + 1 
def quickSort(arr,low,high):
	if low < high:
		mid = partition(arr,0,len(arr)-1)
		quickSort(arr,low,mid-1)
		quickSort(arr,mid+1,high)
		
arr = [10, 7, 8, 9, 1, 5]
n = len(arr)
quickSort(arr,0,n-1)

关于快速排序的最好、最坏、平均 时间复杂度分析。
快速排序是一个不稳定的排序方法。

简单描述一下,快速排序的方法是用第一个数或者是最后一个数作为基准pivot,然后对arr数组遍历。用 i 来代表排序后比pivot 小的最后位置。最后返回i +1 代表划分为两个区间的位置。

(1)现在分析最优情况是:
每次找到的pivot刚好就能把数组arr 平均分成两半。两边的树平衡。
在这里插入图片描述
如果数组长度为N,那么根据平衡的递归树的定义则 树的深度 = log(N) + 1
时间复杂度计算(最优情况)
第一轮:遍历arr数组的所有数 N 然后刚好 pivot落在正中心,所以是时间复杂度是 T(N) 是这一轮的遍历N 加上左右两个区间的时间复杂度T(N/2).
T(N) = 2T(N/2) + N

第二轮:同理对左边的区间N/2的数组遍历。 所以有
T(N) = 2(2T(N/4) + N/2) + N = 4 T(N/4) + 2N

第三轮: T(N) = 2(2( 2T(N/8) + N/4) + N/2) + N = 8T(N/8) + 3N

第四轮:T(N) = 16T(N/16) + 4N


第k轮: T(N) = 2 k T ( N / 2 k ) + k N 2^kT(N/2^k) + kN 2kT(N/2k)+kN

由于我们知道树的深度是 log(N) 带入得到
T ( N ) = 2 l o g ( N ) T ( N / 2 l o g ( N ) ) + ( l o g ( N ) ) N T(N) = 2^{log(N)}T(N/2^{log(N) }) + (log(N) )N T(N)=2log(N)T(N/2log(N))+(log(N))N
所以
T(N) = N T(1) + N(logN) = N + NlogN
所以时间复杂度是 O(NlogN) 即O(nlogn)
所以最优情况的时间复杂度是 O(nlogn)

(2)最坏的情况是 数组是正序或者倒序
当数组是正序或者倒序,每次划分只得到一个比上一次划分少一个记录的子序列,另一个子序列为空,所以组成的递归树是一颗偏斜的树,需要执行n‐1次递归调用。
所以比较的次数是
N + (N-1) + (N-2) + (N-3) +, + 1 = (N+1)N/2
所以时间复杂度是 O(N^2)

【PS:平均时间复杂度可以当作是 一次坏的划分,接着是一次好的划分,轮着来,最后的时间复杂度也是O(nlogn).】

空间复杂度是O(logn)
快排的空间复杂度是O(logn),因为快排的实现是递归调用的, 而且每次函数调用中只使用了常数的空间,因此空间复杂度等于递归深度O(logn)

(4)并归排序

分治法 是讲一个难以直接解决的问题,分成几个简单的问题,最后再把这些简单的问题合并在一起。
步骤:
(1)讲原问题分解成若干个小问题
(2)如果小问题可以直接合并,就直接合并 不用递进
(3)合并各个小问题

举一个小例子就是 先把 nums的所有元素分开 一半,然后一直分,分成一个个单独的[num],再两两排序合并
具体怎么合并呢?用双指针来做 p、q放在两个数组第一位,然后谁小放入ans里面

def merge(left,right):
	#合并两个有序数组
	ans = []
	p,q =0,0 
	while p < len(left) and q < len(right):
		if left[p] < right[q]:
			ans.append(left[p])
			p+=1 
		else: 
			ans.append(right[q])
			q+=1 
			
	ans += left[p:]
	ans += right[q:]
	return ans  
	

def mergeSort(nums):
	if len(nums)<=1:return nums
	#分成两半
	mid = len(nums)//2
	left_seq = nums[:mid]
	right_seq = nums[mid:]
	left = mergeSort(left_seq)
	right = mergeSort(right_seq)
	return merge(left,right)

并归排序的时间复杂度是O(nlogn)
这个时间复杂度是稳定的。不随着排序的序列不同而产生波动

讲一下这个时间复杂度是怎么计算的
假设nums长度为N
递归的第一层是 分成 两个区间 长度分别为N/2
递归的第二层将分成 4个区间长度为N/4
递归的第三层分成8个区间,长度为N/8

等分成N个区间的时候,每个区间长度都为1。 这个时候递归了多少次?
答案是 假设为X次,则有
2^X = N 所以,X = logN
所以递归了logN层,

第二部合并
在第logN层的时候,每个子区间长度都是1 , 对每相邻的子区间合并。n个数字都会被遍历一次,,
所以这一层的时间复杂度是 O(N)
、、、、
轮到第二层的时候,每个区间的长度为 N/4 总共有4个区间,总共合并两次,n个数字都会被遍历。所以这一层的时间复杂度也是 O(N)
第一层也是一样的道理
所以 合并的时候 每一层都会对n个元素操作,每一层的时间复杂度都是 O(N)。总共有logN层
所以总的时间复杂度是 O(NlogN)

(5)堆排序

最大堆:最大堆中的最大元素在根结点(堆顶);堆中每个父节点的元素值都大于等于其子结点(如果子节点存在)

最小堆:最小堆中的最小元素出现在根结点(堆顶);堆中每个父节点的元素值都小于等于其子结点(如果子节点存在)

堆排序的过程可以具体分为三步,创建堆,调整堆,堆排序

核心是 先创建一个无序的堆,然后如果父节点为 i,
则它的左节点位置是 2i + 1 ; 右节点是2i + 2;
所以 要比较 把其中数值最大放在父节点上。不断的重复,等到最大的数放在根那里,就可以进行交换,放在末尾端。
不断重复上面的操作。

code:
对父节点以及对应的左右节点:做比较并进行交换把最大值放在父节点上。

def heapify(arr,size,rootIndex):
  left = 2*rootIndex +1 
  right = 2*rootIndex + 2 
  maxIndex = rootIndex 
  if left < size and arr[maxIndex] < arr[left]: maxIndex = left 
  if right < size and arr[maxIndex] < arr[right]: maxIndex = right
  if maxIndex != rootIndex: 
    arr[maxIndex], arr[rootIndex] = arr[rootIndex], arr[maxIndex]
    #进行下一个节点的交换 这次的maxIndex 已经改变了
    heapify(arr,size,maxIndex)

def heapSort(arr):
  #从最后一个节点开始
  for i in range(len(arr)-1,-1,-1):
    heapify(arr,len(arr),i)
  #上面命令建好了最大堆
  #下面是进行交换 把建立好的最大堆的根节点 也就是最大值 放在列表的末端 
  #然后再重复
  for i in range(len(arr)-1,-1,-1):
    arr[i] ,arr[0] = arr[0],arr[i]
    #互换之后,堆中就少了一个元素,所以当前堆的个数变了,变为i
    #此时由于堆只变了根节点,因此只需要对根节点进行build_max
    heapify(arr,i,0)
  return arr

时间复杂度都是nlogn,但不稳定
【PS:补充一个点 内置函数sort的时间复杂度是nlogn; min和max的时间复杂度是n】

(6)桶排序

桶排序(Bucket sort)是一种通过分桶和合并实现的排序算法,又被称为箱排序。

桶排序先将数据分到有限数量的桶里,然后对每一个桶内的数据进行排序(桶内排序可以使用任何一种排序算法,如快速排序),最后将所有排好序的桶合并成一个有序序列,列表排序完成。

def bucket_sort(arr):
	min_, max_ = min(arr),max(arr)
	bucket_num = (max_ - min_)//3+1
	k = int(bucket_num)
	buckets = [[] for _ in range(k)]
	for num in arr:
		buckets[int(num - min_)//3].append(num)
	#上面的过程是将数组 按大小放入这个四个桶里
	#每个通里面是无序的,但后面的桶的最小值都会大于前面的最大值
	#这部分的时间复杂度是N+N+N = 3N 
	=====
	ans =[]
	for i in buckets:
		for j in sorted(i):
			ans.append(j)
	#这部分的时间复杂度是 n log(n/4) = n(logn - log4) = nlogn - nlog4 
	#最后的时间复杂度是 nlogn + 3n - nlog4 
	#时间复杂度是O(nlogn)
	return ans
 

第一部分是:
上面的过程是将数组 按大小放入这个四个桶里
每个通里面是无序的,但后面的桶的最小值都会大于前面的最大值
这部分的时间复杂度是N+N+N = 3N = O(N)
第二部分:
这部分的时间复杂度是 n/m log(n/m) *m= n(logn - logm) = nlogn - nlogm
最后的时间复杂度是 nlogn + 3n - nlog4
时间复杂度是O(n + nlogn - nlogm). m代表的是桶数

如果相对于同样的N,桶数量M越大,其效率越高,这个时候nlogn - nlogm = 0。 最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。 此外,桶排序是稳定的。

补充:sort VS sorted

(1) sort 是应用在list 上的方法,sorted 可以对所有可迭代的对象进行排序操作
(2)list 的sort 方法返回的是对已经存在的列表进行操作,无返回值; 而内建函数sorted 方法返回的是一个新的list,而不是在原来的基础上进行的操作。

比如常见的 list.sort() 是在list上做的排序 然后也没有返回值
而 sorted(list)是可以返回全新的list

时间复杂度分析:
最差的情况是 需要对每一个桶都要做一次的排序 那么时间复杂度是
N+N+N^2 = N ^2
最优的情况是刚好分在桶里都是排序好的 不需要排序 时间复杂度是 O(N)

(7)插入排序

但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

def insertionSort(arr):
	for i in range(len(arr)):
		pre_index = i-1
		current_val = arr[i]
		while pre_index >= 0 and arr[pre_index] > current_val:
			arr[pre_index+1] = arr[pre_index]
			pre_index -= 1
		arr[pre_index+1] = current_val 
	return arr  

最坏的时间复杂度是N平方,最好的是O(1)
后面还有
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
这两种排序比较偏。不知道会不会被问到

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
常见的几种排序算法有: 1. 冒泡排序(Bubble Sort) 2. 插入排序(Insertion Sort) 3. 选择排序(Selection Sort) 4. 快速排序(Quick Sort) 5. 归并排序(Merge Sort) 下面分别给出用Python实现这些排序算法的代码: 1. 冒泡排序 ```python def bubble_sort(arr): n = len(arr) for i in range(n): for j in range(0, n-i-1): if arr[j] > arr[j+1]: arr[j], arr[j+1] = arr[j+1], arr[j] return arr ``` 2. 插入排序 ```python def insertion_sort(arr): for i in range(1, len(arr)): key = arr[i] j = i-1 while j >=0 and key < arr[j] : arr[j+1] = arr[j] j -= 1 arr[j+1] = key return arr ``` 3. 选择排序 ```python def selection_sort(arr): n = len(arr) for i in range(n): min_idx = i for j in range(i+1, n): if arr[min_idx] > arr[j]: min_idx = j arr[i], arr[min_idx] = arr[min_idx], arr[i] return arr ``` 4. 快速排序 ```python def quick_sort(arr): if len(arr) <= 1: return arr else: pivot = arr[0] left = [] right = [] for i in arr[1:]: if i < pivot: left.append(i) else: right.append(i) return quick_sort(left) + [pivot] + quick_sort(right) ``` 5. 归并排序 ```python def merge_sort(arr): if len(arr) > 1: mid = len(arr)//2 L = arr[:mid] R = arr[mid:] merge_sort(L) merge_sort(R) i = j = k = 0 while i < len(L) and j < len(R): if L[i] < R[j]: arr[k] = L[i] i += 1 else: arr[k] = R[j] j += 1 k += 1 while i < len(L): arr[k] = L[i] i += 1 k += 1 while j < len(R): arr[k] = R[j] j += 1 k += 1 return arr ``` 以上就是几种常见的排序算法Python实现
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jianafeng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值