2.1 排序的概念
将一组“无序”的记录序列调整为“有序”的记录序列
列表排序:将无序列表变为有序列表
- 输入:列表
- 输出:有序列表
内置排序函数:sort()
2.2 常见排序算法
排序不太好的三人组:冒泡排序、选择排序、插入排序,时间复杂度都为 O()
排序比较好的三人组:快速排序(O(nlogn))、堆排序(O(nlogn))、归并排序(O(nlogn),空间复杂度O(n))
其他排序:希尔排序、计数排序、基数排序
2.3 冒泡排序(Bubble Sort)
列表每两个相邻的数,如果前面比后面大,则交换这两个数。一趟排序完成后,则无序区减少一个数,有序区增加一个数。
代码关键点:躺、无序区范围
# 冒泡排序
# 排序了n-1躺
import random
# 升序
def bubble_sort(list1):
for i in range(len(list1) - 1): # 第i躺
exchange = False # 标志的作用是看有没有发生交换,False代表没交换
for j in range(0, len(list1) - i - 1): # 指针动向
if list1[j] > list1[j+1]: # 如果前面一个数比后面的数大就交换
list1[j], list1[j+1] = list1[j+1], list1[j] # 交换
exchange = True # 交换了为True
print(list1)
if not exchange: # 如果不需要交换就直接结束
return
# 降序
'''
def bubble_sort(list1):
for i in range(len(list1) - 1): # 第i躺
for j in range(0, len(list1) - i - 1): # 指针动向
if list1[j] < list1[j+1]: # 如果前面一个数比后面的数小就交换
list1[j], list1[j+1] = list1[j+1], list1[j] # 交换
'''
list1 = [random.randint(0, 100) for i in range(10)]
print(list1)
bubble_sort(list1)
2.4 选择排序(Select Sort)
一趟排序记录最小的数,放到第一个位置;
再一趟排序记录列表无序区最小的数,放到第二个位置;
...以此类推
算法关键点:有序区和无序区、无序区最小数的位置
# 选择排序
# 简单选择,生成了两个列表浪费内存
'''
def select_sort_simple(li):
li_new = []
for i in range(len(li)):
min_val = min(li) # min()复杂度为o(n)
li_new.append(min_val)
li.remove(min_val) # remove()复杂度为o(n)
return li_new
li = [2, 5, 4, 8, 9, 1, 6]
print(select_sort_simple(li))
'''
def select_sort(li):
# 为了不和上面的多开辟一个列表,把那个最小的数放在哪儿比较合适呢?最前面或者最后面
# 最小的数与第一个位置的数交换就可以实现把最小的数放在第一个位置
for i in range(len(li) - 1): # i表示第几趟
# 先假定无序区的第一个就是最小值
min_loc = i # 下标
# 第1趟是1到最后,第i趟就是i到最后
for j in range(i+1, len(li)): # 可以是i,i+1表示不用跟自己比
# 进行最小的数对比
if li[j] < li[min_loc]:
min_loc = j # 进行换位
# 找到了最小的值,和无序区第一个最小的值交换
li[i], li[min_loc] = li[min_loc], li[i]
print(li) # 每一趟的过程
li = [2, 5, 4, 8, 9, 1, 6]
print('待排序:%s' % li)
select_sort(li)
2.5 插入排序(Insert Sort)
初始时手里(有序区)只有一张牌,每次(从无序区)摸一张牌。插入到手里已有牌的正确位置
# 插入排序
def insert_sort(li):
for i in range(1, len(li)): # i表示摸到的牌的下标
tmp = li[i]
j = i - 1 # j指的时手里的牌的下标
while j >= 0 and li[j] > tmp: # 当满足摸到的牌比j所指的牌小时,手里的牌往右移,知道找到可以插入的位置
li[j+1] = li[j]
j -= 1
li[j+1] = tmp # 把摸到的牌放到j所指的后一个空位置
print(li) # 打印每一趟排序过程
li = [3, 2, 4, 1, 5, 7, 9, 6, 8]
insert_sort(li)
2.6 快速排序
快速排序思路:
取一个元素p(第一个元素,枢轴),使元素p归位;
列表被p分成两部分,左边都比p小,右边都比p大;
递归完成排序
时间复杂度为O(nlogn)
# 快速排序
def partition(li, left, right):
tmp = li[left]
while left < right:
while left < right and li[right] >= tmp: # 从右侧找出比tmp小的数,如果比枢轴的数大,那就往前找
right -= 1 # right往前(左)一个位置
# 找到了比枢轴小的数,给left所指位置赋值
li[left] = li[right]
# 开始从左边找比枢轴大的数,如果找到了比枢轴大的数终止循环
while left < right and li[left] <= tmp:
left += 1 # left往后(右)一个位置
# 找到了比枢轴大的数,给right所指位置赋值
li[right] = li[left]
li[left] = tmp # 把tmp归为
print(li)
return left
def quick_sort(li, left, right):
if left < right: # 至少两个元素,递归出口
mid = partition(li, left, right)
quick_sort(li, left, mid-1)
quick_sort(li, mid+1, right)
li = [5, 7, 4, 6, 3, 1, 2, 9, 8]
quick_sort(li, 0, len(li) - 1)
2.7 堆排序
堆:一种特殊的完全二叉树结构
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大
小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小
堆排序过程:
1.建立堆
2.得到堆顶元素,为最大元素
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序
4.堆顶元素为第二大元素
5.重复步骤3,直到堆变空
import random
# 堆排序
def sift(li, low, high): # low是堆的根节点,high:堆的最后一个元素的位置
i = low # i最开始指向根节点
j = 2 * i + 1 # j最开始是左孩子
tmp = li[low] # 把堆顶存起来
while j <= high: # 只要j位置有数
if j + 1 <= high and li[j+1] > li[j]: # 如果右孩子有并且比较大
j = j + 1 # j指向右孩子
if li[j] > tmp:
li[i] = li[j] # 把j位置得元素放在i位置上
i = j # 往下看一层
j = 2 * i + 1
else: # tmp更大,把tmp放到i得位置上
li[i] = tmp # 把tmp放到某一级领导位置上
break
else:
li[i] = tmp
def heap_sort(li):
n = len(li)
# 中间的-1表示倒着遍历到0
for i in range((n-2)//2, -1 ,-1):
# i表示建堆得时候调整得部分的根的下标
sift(li, i, n-1)
# 建堆完成了
for i in range(n-1, -1, -1):
# i 指向当前堆的最后一个元素
li[0], li[i] = li[i], li[0]
sift(li, 0, i - 1) # i-1是新的high
print(li)
li = [i for i in range(10)]
random.shuffle(li)
print(li)
heap_sort(li)
Python中有内置模块:heapq
常用函数:
- heapify(x):建堆(默认小根堆)
- heappush(heap,item):加入元素
- heappop(heap):弹出元素
topk问题:现在又n个数,设计算法得到前k大的数。(k<n)
解决思路:
- 排序后切片 O(nlogn)
- 排序不太好三人组 O(mn)
- 堆排序思路 O(nlogk)
按照堆排序思路:
- 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数;
- 以此向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换成为该元素并且对堆进行一次调整;
- 遍历列表所有元素后,倒序弹出堆顶;
import random
# 堆排序
def sift(li, low, high): # low是堆的根节点,high:堆的最后一个元素的位置
i = low # i最开始指向根节点
j = 2 * i + 1 # j最开始是左孩子
tmp = li[low] # 把堆顶存起来
while j <= high: # 只要j位置有数
if j + 1 <= high and li[j+1] < li[j]: # 如果右孩子有并且比较小
j = j + 1 # j指向右孩子
if li[j] < tmp:
li[i] = li[j] # 把j位置得元素放在i位置上
i = j # 往下看一层
j = 2 * i + 1
else: # tmp更小,把tmp放到i得位置上
li[i] = tmp # 把tmp放到某一级领导位置上
break
else:
li[i] = tmp
def topk(li, k):
heap = li[0:k]
# 1.建堆
for i in range((k-2)//2, -1, -1):
sift(heap, i, k-1)
# 2.遍历
for i in range(k, len(li) - 1):
if li[i] > heap[0]:
heap[0] = li[i]
sift(heap, 0, k-1)
# 3.出数
for i in range(k-1, -1, -1):
# i 指向当前堆的最后一个元素
heap[0], heap[i] = heap[i], heap[0]
sift(heap, 0, i - 1) # i-1是新的high
return heap
li = [i for i in range(10)]
random.shuffle(li)
print(li)
print(topk(li, 5))
2.8 归并排序
假设现在的列表分两段有序,如何将其合成为一个有序列表--->归并
使用归并排序步骤:
- 分解:将列表越分越小,直至分成一个元素;
- 终止条件:一个元素是有序的;
- 合并:将两个有序列表归并,列表越来越大
# 归并排序
import random
def merge(li, low, mid, high):
i = low
j = mid + 1
ltmp = []
while i <= mid and j <= high: # 只要左右两边都有数
# 比一下这两个数哪个更小那个更大
if li[i] < li[j]:
ltmp.append(li[i])
i += 1
else:
ltmp.append(li[j])
j += 1
# while执行完,肯定有一部分没数了
while i <= mid:
ltmp.append(li[i])
i += 1
while j <= high:
ltmp.append(li[j])
j += 1
li[low:high+1] = ltmp
# i = [2, 4, 5, 7, 1, 3, 6, 8]
# merge(li, 0, 3, 7)
# print(li)
def merge_sort(li, low, high):
if low < high: # 至少有两个元素,递归
mid = (low + high) // 2
merge_sort(li, low, mid)
merge_sort(li, mid+1, high)
merge(li, low, mid, high)
print(li)
li = list(range(10))
random.shuffle(li)
print(li)
merge_sort(li, 0, len(li) - 1)
2.9 希尔排序(Shell Sort)
希尔排序是一种分组插入排序算法。
步骤:
- 首先取一个整数=n/2,将元素分为个组,每组相邻量元素之间距离为,在各组内进行直接插入排序;
- 取第二个整数=/2,重复上述分组排序过程,直到=1,即所有元素在同一组内进行直接插入排序;
- 希尔排序每趟并不使某些元素有序,而是使整体数据越来越接近有序;最后一趟排序使得所有数据有序。
# 希尔排序
import random
def insert_sort_gap(li, gap):
for i in range(gap, len(li)): # i表示摸到的牌的下标
tmp = li[i]
j = i - gap # j指的时手里的牌的下标
while j >= 0 and li[j] > tmp: # 当满足摸到的牌比j所指的牌小时,手里的牌往右移,知道找到可以插入的位置
li[j+gap] = li[j]
j -= gap
li[j+gap] = tmp # 把摸到的牌放到j所指的后gap个位置
#print(li) # 打印每一趟排序过程
def shell_sort(li):
d = len(li) // 2
while d >= 1:
insert_sort_gap(li, d)
d //= 2
print(li)
li = list(range(10))
random.shuffle(li)
print(li)
shell_sort(li)
2.10 计数排序
对列表进行排序,已知列表中的数范围都在0到100之间。设计时间复杂度为O(n)的算法。(有很很多条件限制)
# 计数排序
import random
def count_sort(li, max_count=100):
# 当循环体内不需要用到自定义变量,可将自定义变量改为下划线,使用range()来代表循环的次数
count = [0 for _ in range(max_count+1)]
for val in li:
count[val] += 1
li.clear()
for ind, val in enumerate(count):
for i in range(val):
li.append(ind)
print(li)
li = [random.randint(0, 100) for _ in range(100)]
print(li)
count_sort(li)
2.11 桶排序(Bucket Sort)
在计数排序中,如果元素的范围比较大(比如在1到1亿之间),如何改造算法?
桶排序:首先将元素分在不同的桶中,在对每个桶中的元素排序。
# 桶排序
# n为桶的数量,一共有10000个数
import random
def bucket_sort(li, n=100, max_num=10000):
buckets = [[] for _ in range(n)] # 创建桶
for var in li:
# 0-99->>>0号桶,100-199->>>1号桶
i = min(var // (max_num // n), n-1) # i 表示var放到几号桶里
buckets[i].append(var) # 把元素var放进桶里
# 保持桶内的顺序
for j in range(len(buckets[i])-1, 0, -1):
if buckets[i][j] < buckets[i][j-1]:
buckets[i][j], buckets[i][j-1] = buckets[i][j-1], buckets[i][j]
else:
break
sorted_li = []
for buc in buckets:
sorted_li.extend(buc) # 把buc列表加在sorted_li列表后面
print(li)
li = [random.randint(0, 10000) for i in range(100000)]
print(li)
li = bucket_sort(li)
桶排序的表现取决于数据的分布。也就是需要对不同数据排序时采取不同的分桶策略。
平均情况时间复杂度:O(n+k)
最坏情况时间复杂度:O(k)
空间复杂度:O(nk)
2.12 基数排序
多关键字排序:假如现在有一个员工表,要求按照薪资排序,年龄相同的员工按照年龄排序。(可以先按照年龄进行排序,再按照薪资进行稳定的排序)
对32,13,94,52,17,54,93排序,是否可以看做多关键字排序?
# 基数排序
import random
def radix_sort(li):
max_num = max(li) # 最大值99要经过2次比较,888需要做3次比较,10000要经过5次比较
it = 0
# 10的it次方
while 10 ** it <= max_num:
buckets = [[] for _ in range(10)] # 10个桶
for var in li:
# 987 it=1 就等同于取8出来->>>987//10->98,98%10->8;it = 3 ->>> 987//10->9 9%10 还是9
digit = (var // 10 ** it) % 10
buckets[digit].append(var)
# for循环结束表示分桶完成
# 把数重新写回li
li.clear()
for buc in buckets:
li.extend(buc) # 把buc列表加在sorted_li列表后面
it += 1
print(li)
li = list(range(100))
random.shuffle(li)
print(li)
radix_sort(li)
2.13 查找排序相关面试题
(1)给两个字符串s和t,判断t是否为s的重新排列后组成的单词。
s = 'anagram',t='nagaram',return tue.
s='rat',t='car',return false.
# 先把两个字符串字母排序再对比
class Solution:
# 时间复杂度比较高
def isAnagram_1(self, s, t):
'''
:param s: str
:param t: str
:return: bool
'''
ss = list(s)
tt = list(t)
ss.sort()
tt.sort()
return ss == tt
# 还可以写成一行:return sorted(list(s)) == sorted(list(t))
# 只要两个字符串中字母一致就可以
def isAnagram_2(self, s, t):
'''
:param s: str
:param t: str
:return: bool
'''
dict1 = {} # {'a':1, 'b':2}
dict2 = {}
for ch in s:
dict1[ch] = dict1.get(ch, 0) + 1 # 如果键存在就原来的值+1,键不存在默认为0再加1
for ch in t:
dict2[ch] = dict2.get(ch, 0) + 1
return dict1 == dict2
(2)给定一个m*n的二位列表,查找一个数是否存在。列表有以下特性:
每一行的列表是从左到右已经排序好;
每一行的第一个数比上一行最后一个数大
class Solution:
def searchMatrix_1(self, matrix, target):
'''
:param matrix: list[list[int]]
:param target: int
:return: bool
'''
# 就是单纯的一个一个来找(变相的线性查找),O(n*n)
for line in matrix:
if target in line:
return True
return False
def searchMatrix_2(self, matrix, target):
'''
:param matrix: list[list[int]]
:param target: int
:return: bool
'''
# 二分查找
h = len(matrix) # 几行
# 边界条件
if h == 0:
return False
w = len(matrix[0]) # 几列
# 边界条件
if w == 0:
return False
left = 0
right = w * h - 1 # 把二维看成一维
'''
0 1 2 3
4 5 6 7
8 9 10 11
通过观察可得,行->>>i = num // 4,列->>>j = num % 4
'''
while left <= right:
mid = (left + right) // 2 # //为向下取整
i = mid // w # 行标
j = mid % w # 列标
if matrix[i][j] == target:
return True
# 证明要找的数在mid的左侧
elif matrix[i][j] > target:
right = mid - 1
else: # list1[mid] < target,待查找的值在mid右侧
left = mid + 1
else:
return False
(3)给定一个列表和一个整数,设计算法找到两个数的下标,使得两个数之和为给定的整数。保证肯定仅有一个结果。
例如:列表[1, 2, 5, 4]与目标整数3,1+2=3,结果为(0, 1)
class Solution:
def twoSum_1(self, nums, target):
'''
:param nums:list[int]
:param target:int
:return:list[int]
'''
n = len(nums)
for i in range(n):
# 只和后面的数比
for j in range(i+1, n):
if nums[i] + nums[j] == target:
return sorted([i, j])
# 二分查找(折半查找)
def binary_search(self, list1, left, right, val):
# 证明候选区有值
while left <= right:
mid = (left + right) // 2 # //为向下取整
if list1[mid] == val:
return mid
# 证明要找的数在mid的左侧
elif list1[mid] > val:
right = mid - 1
else: # list1[mid] < val,待查找的值在mid右侧
left = mid + 1
else:
return None
def twoSum_2(self, nums, target):
'''
:param nums:list[int]
:param target:int
:return:list[int]
'''
n = len(nums)
# 根据一个数找另一个数
for i in range(n):
a = nums[i]
b = target - a # 查找b是在nums中的位置
# 利用二分查找来找b,但有个问题->>>列表不一定有序
# 先试一试如果列表有序会怎么写
if b >= a: # b比a大证明要往后找
j = self.binary_search(nums, i+1, len(nums)-1, b)
else: # b比a小证明要往前找
j = self.binary_search(nums, 0, i-1, b)
if j:
break
return sorted([i, j])
def binary_search_1(self, list1, left, right, val):
# 证明候选区有值
while left <= right:
mid = (left + right) // 2 # //为向下取整
if list1[mid][0] == val:
return mid
# 证明要找的数在mid的左侧
elif list1[mid][0] > val:
right = mid - 1
else: # list1[mid] < val,待查找的值在mid右侧
left = mid + 1
else:
return None
def twoSum_3(self, nums, target):
'''
:param nums:list[int]
:param target:int
:return:list[int]
'''
# 无序列表怎么用二分查找
# 要对nums排序并且记住无序之前的下标
new_nums = [[num, i] for i, num in enumerate(nums)] # i表示原来的下标,num表示数
new_nums.sort(key=lambda x: x[0]) # 按照num排序
for i in range(len(new_nums)):
a = new_nums[i][0]
b = target - a # 查找b是在nums中的位置
# 利用二分查找来找b,但有个问题->>>列表不一定有序
# 先试一试如果列表有序会怎么写
if b >= a: # b比a大证明要往后找
j = self.binary_search_1(new_nums, i + 1, len(new_nums) - 1, b)
else: # b比a小证明要往前找
j = self.binary_search_1(new_nums, 0, i - 1, b)
if j:
break
return sorted([new_nums[i][1], new_nums[j][1]])