(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)
后面还有
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
这两种排序比较偏。不知道会不会被问到