相关术语
稳定性
概念:如果值相同的元素在排序前后保证着排序前的相对位置,则称为稳定的排序,反之则为不稳定排序
时间复杂度和稳定性一览
这里只说平均复杂度
- 冒泡 - O(n²) - 稳定排序
- 插入 - O(n²) - 稳定排序
- 选择 - O(n²) - 稳定排序
- 快速 - O(nlog2(n)) - 不稳定排序
- 归并 - O(nlog2(n)) - 不稳定排序
查找数据(python实现)
暴力查找
从数据集的第一个数据项开始,直到找到最后一个数据项,看是否找得到target
# 无序表中查询
def search(l, target):
for i in l:
if i == target:
return True
return False
二分查找
二分查找只能是在有序表的前提下进行,但是如果是无序表,先需要排序的情况下,可能就会由于不同的排序情况决定时间复杂度
# 有序表中查询数据项,时间复杂度为 O(log2(N))
def bin_search(l, target):
left, right = 0, len(l) - 1
while left <= right:
mid = (right + left) // 2
if l[mid] > target:
right = mid - 1
elif l[mid] < target:
left = mid + 1
else:
return True
return False
排序(python实现)
冒泡排序
类似于河里的水泡一样,由小到大慢慢浮出水面。
- 是指将列表中的数据从第一个开始,两两比较,保证较大的元素排在较小元素的后面,以此类推,直至整个列表全部两两比较完成,此时列表中的最后一个位置应该就为当前列表的最大值。
- 重复上面的步骤,这次比较完的结果应该是列表中倒数第二个数据为次大值,以此类推
- 直至所有元素有序(外部控制循环的次数,为 len(list) - 1次)
# 最直白的方法
def bubble_sort(l):
# 控制冒泡的次数
for i in range(len(l) - 1):
# 比较到当前列表有序位置的前一个即可
for j in range(len(l) - i - 1):
if l[i] > l[i + 1]:
l[i], l[i+1] = l[i+1], l[i]
return l
# 简单改良版(减少列表有序部分的比较次数)
def bubble_sort(l):
for i in range(len(l) - 1):
is_sorted = True
for j in range(len(l) - i - 1):
if l[j] > l[j + 1]:
l[j], l[j+1] = l[j+1], l[j]
# 但凡有交换,就说明当前长度的列表还没有排好序
is_sorted = False
# 内部循环走完为True,说明[:len(l) - i - 1]的顺序已经是有序的,然后又因为外面的大循环把[len(l) - i - 1:]部分元素已经排好序,所以整个列表已经是有序列表,不需要进行重复的判断了
if is_sorted:
break
return l
# 优化版本(记录最后一次交换元素的位置,主要在于后面已经有序的部分不在进行比较)
def bubble_sort(l):
last_position = 0
sort_border = len(l) - 1
# 控制要比较的总次数
for i in range(len(l) - 1):
is_sorted = True
# sort_border后面都是有序的,不需要进行重复比较了
for j in range(sort_border):
if l[j] > l[j + 1]:
l[j], l[j + 1] = l[j + 1], l[j]
is_sorted = False
# 记录最后一次交换的索引位置
last_position = j
sort_border = last_position
if is_sorted:
break
return l
r = bubble_sort(l)
print(r)
选择排序(O(n²))
一般来讲,每次循环默认选定列表第一个位置为当前列表最大值,将此位置的值和后面的值做比对,如果发现更大的值,则记录该值的位置,直到遍历到列表末尾,将末尾的值和最大位置的值交换,这样最大的值便出现在列表的最后一个位置
def selection_sort(l):
if not l:
return
for i in range(len(l) - 1):
# 每一次记录最大值的位置,并和最后一个没排序的位置进行交换
max_index = 0
for j in range(1, len(l)-i):
if l[j] > l[max_index]:
max_index = j
l[-(i + 1)], l[max_index] = l[max_index], l[-(i + 1)]
return l
插入排序
概念:将原列表想象成两部分,前面是已经排好序的,后面是乱序的,依次将乱序部分的每一个数据都和前面排好序的部分进行比较,插入到相应的位置
步骤
- 最开始,将列表第一个数据加入到前面部分,这时**原列表就可看作第一个数据(排好序部分)和剩下的数据(未排序部分)**两部分,并同时将第二个数据(未排序中的第一个数据)和第一个数据(排好序的最后一个数据)进行比对,根据大小插入到相应的位置,此时有两个数据已经有序
- 然后将第三个数据(未排序中的第一个数据)和前两个数据(已经有序的两个)相比较,并移动比自身更大的数据,排序,这样便排好3个数据
- 以此类推,直到整个后面部分(未排序)的数据都添加到前面部分(有序)中
def insertion_sort(l):
for i in range(1, len(l)):
# 将当前元素从后向前和已经排序的进行比较,l[j]就是当前未排序中的第一个元素
for j in range(i, 0, -1):
if l[j] < l[j - 1]:
l[j], l[j - 1] = l[j - 1], l[j]
return l
归并排序
递归,将问题规模变小,直至有个退出的条件,解决每个子问题,最后将所有子问题的答案拼接到一起即可
# 两个方法的核心思想差不多,在于合并操作有点点的区别
# ############################ 第一个方法 ##############################
def merge_sort(l):
if len(l) <= 1:
return l
mid = len(l) // 2
left = merge_sort(l[:mid])
right = merge_sort(l[mid:])
# 这个复杂度要高一点,毕竟涉及到pop操作
result = []
while left and right:
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
result.extend(right if right else left)
return result
# ############################ 第一个方法 ##############################
# ############################ 第二个方法 ##############################
# 这里分成两个部分,一个用来专门递归,控制流程,一个专门用来将递归出来的子列表进行排序操作
# 归并排序
def merge_sort(l):
if len(l) <= 1:
return l
mid = len(l) // 2
left = merge_sort(l[:mid])
right = merge_sort(l[mid:])
return merge(left, right)
def merge(nums1, nums2):
r = []
# 两个指针分别指向两个列表当前数据的索引
left, right = 0, 0
while left < len(nums1) and right < len(nums2):
if nums1[left] <= nums2[right]:
r.append(nums1[left])
left += 1
else:
r.append(nums2[right])
right += 1
r += nums1[left:]
r += nums2[right:]
return r
# ############################ 第二个方法 ##############################
快速排序
概念:快速排序是冒泡排序的升级版,但是平均性能却有很大的提升,达到O(nlog2(n))的时间复杂度,最差情况就和冒泡的性能一样为O(n²)
理解:原来我一直以为快速排序和归并思想上异曲同工,都是通过减小规模的方式进行排序操作,但是深入的了解了才发现两者核心思想完全不是一个方向···这里不做大段的描述性陈述,网上有很多,书上也有很多,细节可以自己去看,只能说,自己多敲几回,自己敲,出bug就想想为什么,实在不行再看正解,自己去想原理,想想就真的通了,以下几点只是把我的理解分享一下···
- 快速排序也是通过元素之间的交换对列表排序
- 函数(partatition)每次执行一次,都能100%保证至少一个元素(prviot元素)在列表中的顺序是正确的(也就是说,每次执行完整个函数,这个列表中至少有一个元素(prviot的数据)被原地更改,并且放到了整个列表他应该所在的位置,好好想想这句话)
- 承接第二点2来说,其他元素有可能有移动,有可能没移动,取决于数据分布状况
- 每次partatition函数返回的结果就是,prviot位置的元素已经位置正确,那么就调整剩下的元素的顺序,分成prviot左边部分以及右边部分,(prviot不需要进行排序操作了)
- 以此类推,每次都能保证至少一个元素有序,其他元素也会根据prviot的值进行相应的调整,直至递归结束条件达成
# 为什么这么写,因为用户只需要提供要排序的序列即可,所以在quick内封装了一个helper函数用来递归,并提供相关参数,这里采用双边循环法,左右各有一个指针指向变化的元素
# 调用接口
def quick_sort(l):
left = 0
right = len(l) - 1
quick_sort_help(l, left, right)
# 辅助函数
def quick_sort_help(l, left, right):
if left < right:
index = partition(l, left, right)
quick_sort_help(l, left, index - 1)
quick_sort_help(l, index + 1, right)
# 选择基准值,并将标准值的左右两边汇聚到一起,并返回基准值位置的索引
def partition(l, left, right):
prviot = l[left]
start = left
end = right
while start < end:
while l[end] >= prviot:
end -= 1
while l[start] <= prviot:
start += 1
if start < end:
l[start], l[end] = l[end], l[start]
l[left], l[end] = l[end], l[left]
return end