写在前面
插入二分查找
def binary_tree(arr, t):
low, high = 0, len(arr) - 1
while low < high:
mid = (low + high) // 2
if t > arr[mid]:
low = mid + 1
elif t < arr[mid]:
high = mid
else:
return mid
return low if arr[low] == t else False
print(binary_tree([1, 2, 3, 4, 5, 6, 7], 2))
时间复杂度O(logn)
1. 关于比较排序和非比较排序
比较排序:需要通过元素之间比较之后再决定先后顺序
非比较排序:非比较排序就是不需要通过元素之间的比较就可以确定每个元素的位置(基数排序)
2. 稳定和不稳定
假设一个数组[7,5,2,5]
如果经过排序后第一个5还在第二个5的前面,那么就是稳定排序
否则就可能是不稳定排序
3. 有的算法用到了自带的max(), 感觉既然都能用max干嘛还那么复杂写那么多代码,于是顺手写了一个只用max的排序算法, 没什么技术含量,不知道那个龟孙告诉我, max, min的时间复杂度是O(1), 我当时也没多想, 就像python牛B,能做到O(1)啊,可是过了一段时间后,细细想想,怎么做到的O(1)? yashi, 分明就是O(n)啊, mmp。
所以下面的算法,时间复杂度极差。
def my_sort(arr):
li = []
for i in range(len(arr)):
li.append(max(arr))
arr.remove(max(arr))
return li
print(my_sort([5, 3, 2, 4, 6]))
对数器
功能:测试自己写的排序函数是否排序正确。
原理:生成随机字符串组成一个待测数组,用自带的sort排序,再用自己写的排序数组,两者相比较,重复执行数次。
import random
def GenerateRandomArray(size, value):
arr = []
for i in range(size):
arr.append(int(value + 1) * random.random() - int(value * random.random()))
# 产生一个随机数列长度为size,数字范围为-value~value, 加到arr里
# arr.append(int(value * random.random()))
# 只生成正数
return arr
def RightMathod(arr):
# 一定正确的方法:自带排序方法
arr.sort()
return arr
def IsRight(times, size, value, func):
# 次数 长度 数字范围 测试函数
succeed = True
for i in range(times):
arr1 = GenerateRandomArray(size, value)
arr2 = arr1[:]
arr3 = arr1[:]
func(arr1)
RightMathod(arr2)
if not arr1 == arr2:
succeed = False
print(arr3)
break
return "ok" if succeed is True else "wrong"
print(IsRight(100, 10, 100, insertion_sort))
# # 次数 长度 数字范围 测试函数
一 很重要的快速排序
-
在数列之中,选择一个元素作为”基准”(pivot),或者叫比较值。
-
数列中所有元素都和这个基准值进行比较,如果比基准值小就移到基准值的左边,如果比基准值大就移到基准值的右边
-
以基准值左右两边的子列作为新数列,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
用自己通俗易懂的话就是说,随便选个基准值,把比基准值大的放在一个空列表,比它小的放在一个空列表里,递归操作
def quick_sort(arr):
"""
快速排序
"""
if len(arr) < 2:
return arr
mid = arr[len(arr) // 2]
left, right = [], []
arr.remove(mid)
for item in arr:
if item >= mid:
right.append(item)
else:
left.append(item)
return quick_sort(left) + [mid] + quick_sort(right)
if __name__ == "__main__":
arr = [2, 3, 1, 34, 143, 89, 21, 4]
print(quick_sort(arr))
再来个极端秀的操作 一句话快排
q = lambda a: a if len(a) < 2 else q([i for i in a[1:] if i <= a[0]]) + [a[0]] + q([i for i in a[1:] if i > a[0]])
if __name__ == "__main__":
arr = [2, 3, 1, 34, 143, 89, 21, 4]
print(q(arr))
关于快排:
不稳定, 比较排序, 最坏时间复杂度为O(n²), 最好时间复杂度O(nlogn), 平均时间复杂度O(nlogn),空间复杂度O(nlogn)。
归并排序与快排 :归并排序与快排两种排序思想都是分而治之,但是它们分解和合并的策略不一样:归并是从中间直接将数列分成两个,而快排是比较后将小的放左边大的放右边,所以在合并的时候归并排序还是需要将两个数列重新再次排序,而快排则是直接合并不再需要排序,所以快排比归并排序更高效一些
快速排序有一个缺点就是对于小规模的数据集性能不是很好,按以下方法
-
先使用快排对数据集进行排序,此时的数据集已经达到了基本有序的状态
-
然后当分区的规模达到一定小时,便停止快速排序算法,而是改用插入排序,因为我们之前讲过插入排序在对基本有序的数据集排序有着接近线性的复杂度,性能比较好。
二 冒泡排序
就是说从前往后遍历,两个两个来,两个两个比较,把大的往后排,一直排到最后。再从头开始,接着上面的步骤。
def bubble_sort(arr):
# 循环的遍数
for i in range(len(arr) - 1):
for j in range(len(arr) - 1 - i):
if arr[j] > arr[j + 1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
if __name__ == "__main__":
arr = [2, 3, 1, 34, 143, 89, 21, 4]
print(bubble_sort(arr))
关于冒泡排序:
稳定排序,比较排序,时间复杂度最坏和平均O(n²), 空间复杂度O(1)
三 插入排序
-
从第二个元素开始和前面的元素进行比较,如果前面的元素比当前元素大,则将前面元素 后移,当前元素依次往前,直到找到比它小或等于它的元素插入在其后面
-
然后选择第三个元素,重复上述操作,进行插入
-
依次选择到最后一个元素,插入后即完成所有排序
def insertion_sort(arr):
for i in range(1, len(arr)):
current = arr[i]
pre_index = i - 1
while pre_index >= 0 and arr[pre_index] > current:
# 如果上一个值大于当前值,则交换当前值和其前一个值
arr[pre_index + 1], arr[pre_index] = arr[pre_index], current
pre_index -= 1
return arr
if __name__ == "__main__":
arr = [2, 3, 1, 34, 143, 89, 21, 4]
print(insertion_sort(arr))
关于插入排序:
比较排序,稳定排序, 时间最坏和平均复杂度O(n²),空间复杂度O(1)
四 插入增强版的希尔排序
希尔排序的整体思想是将固定间隔的几个元素之间排序,然后再缩小这个间隔。这样到最后数列就成为了基本有序数列,而前面我们讲过插入排序对基本有序数列排序效果较好。
-
计算一个增量(间隔)值
-
对元素进行增量元素进行比较,比如增量值为7,那么就对0,7,14,21…个元素进行插入排序
-
然后对1,8,15…进行排序,依次递增进行排序
-
所有元素排序完后,缩小增量比如为3,然后又重复上述第2,3步
-
最后缩小增量至1时,数列已经基本有序,最后一遍普通插入即可
已知的最增量式是由 Sedgewick 提出的 (1, 5, 19, 41, 109,…),该步长的项来自 9 4^i - 9 2^i + 1 和 4^i - 3 2^i + 1 这两个算式。这项研究也表明 “比较在希尔排序中是最主要的操作,而不是交换。 用这样增量式的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比*快速排序慢。
def shell_sort(arr):
gap = len(arr) // 2
# 计算增量
while gap > 0:
for i in range(gap, len(arr)):
j = i
current = arr[i]
print(i, current)
while j - gap >= 0 and current < arr[j - gap]:
# 当前值和上一列的数比
arr[j] = arr[j - gap]
j -= gap
arr[j] = current
gap //= 2
return arr
if __name__ == "__main__":
arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(shell_sort(arr))
关于希尔排序:
比较排序,因为希尔排序是间隔的插入,所以存在相同元素相对顺序被打乱,所以是不稳定排序
最坏的时间复杂度O(n²),平均时间复杂度O(n^1.3) 这个时间复杂度有争议
空间复杂度O(1)
五 没鸡儿用的选择排序
-
设第一个元素为比较元素,依次和后面的元素比较,比较完所有元素找到最小的元素,将它和第一个元素互换
-
重复上述操作,我们找出第二小的元素和第二个位置的元素互换,以此类推找出剩余最小元素将它换到前面,即完成排序
def selection_sort(arr):
for i in range(len(arr)):
# 将第一个元素设为最小元素
min_index = i
# 从当前位置往后遍历,实时更新最小位置的索引
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_index]:
# 交换当前位置和最小的位置
min_index = j
arr[min_index], arr[i] = arr[i], arr[min_index]
return arr
if __name__ == "__main__":
arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(selection_sort(arr))
关于选择排序:
比较排序,不稳定性(因为存在任意位置的两个元素交换,比如[5, 8, 5, 2],第一个5会和2交换位置,所以改变了两个5原来的相对顺序)
时间复杂度O(n²), 空间复杂度O(1)
六 归并排序
递归合并
归并排序是分治法的典型应用。分治法(Divide-and-Conquer):将原问题划分成 n 个规模较小而结构与原问题相似的子问题;递归地解决这些问题,然后再合并其结果,就得到原问题的解。从上图看分解后的数列很像一个二叉树。
-
使用递归将源数列使用二分法分成多个子列
-
申请空间将两个子列排序合并然后返回
-
将所有子列一步一步合并最后完成排序
def merge_sort(arr):
# 归并排序
if len(arr) == 1:
return arr
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
return merge(merge_sort(left), merge_sort(right))
def merge(left, right):
result = []
while len(left) > 0 and len(right) > 0:
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
result += left
result += right
return result
if __name__ == "__main__":
arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
print(merge_sort(arr))
关于归并:
比较排序,稳定排序,最坏和平均时间复杂度O(nlogn), 空间复杂度O(n)
七 神奇的桶排
如图,先建立max-min+1个桶,如图是8-1+1=8个,然后对应的num=当前值-min,给num号的桶加个记号。然后再把桶还原成数组。采用分治的方法。
下面的桶排不支持负数的比较
def bucketSort(nums):
# 创建桶, max-min_1个桶
bucket = [0]*(max(nums)-min(nums)+1)
for i in nums:
# 把数值按照大小的不同求出索引,让然后让对应的桶序号加一
bucket[i-min(nums)] += 1
# print(bucket)
# [1, 0, 1, 0, 2, 1]
tmp = []
for i in range(len(bucket)):
# 遍历每个桶, 如果不等于一说明有数
if bucket[i] != 0:
tmp += [min(nums)+i]*bucket[i]
# 如果有两个5, [5] * 2 在tmp里加两个5
return tmp
if __name__ == "__main__":
arr = [3, 6, 1, 8, 6]
print(bucketSort(arr))
关于桶排序
稳定,非比较排序,
平均时间复杂度O(n+k),
最好时间复杂度O(n),
最坏时间复杂度O(n²),
空间复杂O(n+k)
其中k是桶的个数
八 复杂的堆排
利用二叉树的大根堆,把数据看成一颗完全二叉树,逐步调成大根堆的结构,调完一次,0所在的位置就是整个数组里最大的数,然后把0位置和最后位置对调,然后再对数组-1个数进行堆排,重复上面的步骤,直到遍历完。整个过程很想写在前面的那个自己只用max()的算法,先在数组中找到最大的数,加到一个空列表里,再对剩下的数用max找到第二大数,加到列表里,重复执行。
红色为编号,黑色为待排序数组。首先len(arr) // 2 - 1,找到最后一个父节点,看它和它的子节点们谁大,谁大谁就当根节点,逐次遍历,直到把最大节点放到0的根节点,。。。。。。。
def sift_down(arr, start, end):
root = start
while True:
# 从root开始对最大堆调整
child = 2 * root + 1
# 求出父节点的左子节点
if child > end:
# 不要满足完全二叉树
break
# 找出两个child中交大的一个
if child + 1 <= end and arr[child] < arr[child + 1]:
# 前面判断是否有右子节点
child += 1
if arr[root] < arr[child]:
# 最大堆小于较大的child, 交换顺序
arr[root], arr[child] = arr[child], arr[root]
# 正在调整的节点设置为root,在调整下面的结构
root = child
else:
# 无需调整的时候, 退出
break
def heap_sort(arr):
first = len(arr) // 2 - 1
# 求出最后一个节点的父节, 最后一个节点 = root *2 + 1
for start in range(first, -1, -1):
# 从最后一个有父节点的节点开始,一直到0节点,实际遍历的是每一个父节点
# 为什么是len-1?因为加入len=10,而最后一个位置为9, 从零开始
# 对每个父节点执行操作,成为大根堆
sift_down(arr, start, len(arr) - 1)
# 将最大的放到堆的最后一个, 堆-1, 继续调整排序
for end in range(len(arr) - 1, 0, -1):
# 上一个for已经把数组变为大根堆,即0位置是最大的值,
arr[0], arr[end] = arr[end], arr[0]
# 对除了根节点的每个节点操作
sift_down(arr, 0, end - 1)
return arr
关于堆排序
比较排序, 不稳定排序,
最好最坏平均时间复杂度都为O(nlogn), 空间复杂度O(1)
基数排序
类似桶排序如[11, 2, 12, 24, 78, 1], 先建立桶,[[], [], [], [], [], [], [], [], [], []]。
然后按个位排序,[[], [11, 1], [2, 12], [], [24], [], [], [], [78], []]
再按十位排,[[1, 2], [11, 12], [24], [], [], [], [], [78], [], []]
得到结果
def radix_sort(arr):
i = 0 # 记录当前正在排拿一位,最低位为1
max_num = max(arr)
j = len(str(max_num))
# 记录最大值的位数
while i < j:
bucket_list = [[] for _ in range(10)] # 初始化桶数组
# [[], [], [], [], [], [], [], [], [], []]
for x in arr:
bucket_list[int(x // (10**i)) % 10].append(x) # 找到位置放入桶数组
arr.clear()
for x in bucket_list: # 放回原序列
for y in x:
arr.append(y)
i += 1
print(bucket_list)
arr = []
for i in bucket_list:
for j in i:
arr.append(j)
return arr
print(radix_sort([11, 2, 12, 24, 78, 1]))
关于基数排序:
稳定, 非比较排序, 最好最坏平均时间复杂度都是O(n*k), 空间复杂度O(n+k), k是桶的个数