重点掌握二分查找、归并排序、快速排序
不同排序算法的时间复杂度与空间复杂度
1. 冒泡排序
比较相邻的元素,如果第一个比第二个大,就交换。从开始到结尾,这步做完后,最后的元素会是最大的数。然后重复,从开始元素到倒数第二。
a = [1,5,6,0,2,4,1,2,0,5,3]
for i in range(len(a)):
# 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
flag = True
for j in range(len(a)-i-1):
if a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
flag = False
if flag:
break
print(a)
2. 选择排序
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
a = [1,5,6,0,8,22,2,4,1,2,0,5,3]
# 总共要经过 N-1 轮比较
for i in range(len(a)-1):
# 每次重新定义最小值的下标
min_v = i
# 每轮需要比较的次数 N-i
for j in range(i+1, len(a)):
if a[j] < a[min_v]:
# 记录目前能找到的最小值元素的下标
min_v = j
# 最小值交换
if i != min_v:
a[min_v], a[i] = a[i], a[min_v]
print(a)
3. 插入排序
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。
a = [1,5,6,0,8,22,2,4,1,2,0,5,3]
for i in range(len(a)-1):
if a[i] > a[i+1]:
for j in range(i,-1,-1):
if a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
print(a)
4. 归并排序
分割:先设定两个指针start和end,分别为序列的起始位置。求出中间指针,按照中间位置进行递归拆分,分到最细之后对两个有序数列合并。
合并:设定两个指针i和j,比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置,到达某一方指针的尾部后,将另一序列剩下的所有元素直接复制到合并序列尾。
# 将两个有序数列合并
def merge_list(a, l, mid, r):
temp = len(a)*[0]
i = l
j = mid+1
k = 0
# 将左右两个数列合并
while i <= mid and j <= r:
if a[i] <= a[j]:
temp[k] = a[i]
i += 1
k += 1
else:
temp[k] = a[j]
j += 1
k += 1
# 剩余数列值的添加
while i <= mid:
temp[k] = a[i]
i += 1
k += 1
# 剩余数列值的添加
while j <= r:
temp[k] = a[j]
j += 1
k += 1
# 返回到a
for i in range(k):
a[l+i] = temp[i]
def merge_sort(c, start, end):
# mid 如果缺少限制条件,会不断被计算
if start < end:
m = (start + end) // 2
# 左边有序
merge_sort(c, start, m)
# 右边有序
merge_sort(c, m+1, end)
# 将二个有序数列合并
merge_list(c, start, m, end)
xin = [1,5,6,0,8,22,2,4,1,2,0,5,3,32,23,4,-1,3]
merge_sort(xin, 0, len(xin)-1)
5. 快速排序
从数列中挑出一个元素,称为 “基准”(pivot),比基准值小的在左边,比基准值大的在右边,基准在中间位置。递归排序左右子分区,不包含基准本身。
具体实现:设置基准为最右边的元素,初始化一个基准指针,位于区间最左边的位置-1。如果当前元素比基准值小, 基准指针+1,交换当前元素和基准指针对应的元素。基准指针(排序后的位置+1)指向基准的位置,再次交换基准指针对应位置与最右边的元素
与归并排序的区别在于,快速是定基准分区,再在分区里面递归。而归并是先分为左右序列,再合并序列。
import random
# 把数组分为两个部分,比基准值小的摆放在左边,比基准值大的摆在基准的右边。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
def partition(data, l, r):
# 根据区间大小产生随机数,作为基准
p = random.randint(l, r)
# 将基准放在最右边
data[r], data[p] = data[p], data[r]
# 设置一个基准指针,位于区间最左边的位置-1,每摆放一个比基准值小的值,就移动一步
p_position = l - 1
# 所有元素比基准值小的摆放在基准的左边
# 注意!!!此处范围应该是两个指针之间,而不是data的长度!
for i in range(l, r):
if data[i] < data[r]:
p_position += 1
if p_position != i:
data[p_position], data[i] = data[i], data[p_position]
# 基准指针(排序后的位置+1)就是基准的位置
p_position += 1
data[p_position], data[r] = data[r], data[p_position]
return p_position
def quick_sort(c, start, end):
if c == [] or start < 0 or start >= end:
return c
if start < end:
pi = partition(c, start, end)
quick_sort(c, start, pi-1)
quick_sort(c, pi + 1, end)
a = [1,5,6,0,8,22,2,4,1,2,0,5,3,32,23,4,-1,3]
quick_sort(a, 0, len(a)-1)
print(a)
6. 桶排序
设置固定数量的空桶,把数据放到对应的桶中,对每个不为空的桶中数据进行排序,拼接不为空的桶中数据,得到结果。
或者设置(最大值-最小值+1)数量的空桶,把数据放到对应的桶中,拼接不为空的桶中数据,得到结果。
def bucket_sort(c):
buckets = [0] * (max(c) - min(c) + 1)
for i in c:
buckets[i-min(c)] += 1
res = []
for i in range(len(buckets)):
if buckets[i] != 0:
# buckets[i]为次数
res.extend([i + min(c)] * buckets[i])
print(res)
a = [2,6,8,10,12,2,4,8,22,8,8]
bucket_sort(a)
leetcode:153. 寻找旋转排序数组中的最小值
问题描述:假设按照升序排序的数组在预先未知的某个点上进行了旋转。请找出其中最小的元素。你可以假设数组中不存在重复元素。
解法:最小元素正好是两个子数组的分界线,可以用二分查找法
时间复杂度:O(logn)
class Solution:
def findMin(self, nums: List[int]) -> int:
if len(nums) <= 0:
return 0
p1 = 0
p2 = len(nums) - 1
# 没有旋转
if nums[p1] < nums[p2]:
return nums[p1]
while nums[p1] >= nums[p2]:
if p1 == p2 - 1:
return nums[p2]
mid = (p1 + p2) // 2
# mid 为左子数组的递增
if nums[mid] >= nums[p1]:
p1 = mid
# mid 为右子数组的递增
elif nums[mid] <= nums[p2]:
p2 = mid
leetcode:154. 寻找旋转排序数组中的最小值 II
问题描述:假设按照升序排序的数组在预先未知的某个点上进行了旋转。请找出其中最小的元素。注意数组中可能存在重复的元素。
解法:例如{10111}和{11101},p1=p2=mid,无法判断中间的数字位于前面还是后面的递增数组,只能使用顺序查找
时间复杂度:O(logn)或者O(n)
class Solution:
def findMinOrder(self, arr, index1, index2):
res = arr[index1]
for i in range(index1, index2):
if arr[i] < res:
res = arr[i]
return res
def findMin(self, nums: List[int]) -> int:
if len(nums) <= 0:
return 0
p1 = 0
p2 = len(nums) - 1
# 没有旋转
if nums[p1] < nums[p2]:
return nums[p1]
# 如果rotateArray[p1] < rotateArray[p2] 说明是递增数列
while nums[p1] >= nums[p2]:
if p1 == p2 - 1:
return nums[p2]
mid = (p1 + p2) // 2
# 如果p1、p2、mid指向的三个数相等,则只能顺序查找
if nums[mid] == nums[p1] and nums[mid] == nums[p2]:
return self.findMinOrder(nums, p1, p2)
# mid 为左子数组的递增
if nums[mid] >= nums[p1]:
p1 = mid
# mid 为右子数组的递增
elif nums[mid] <= nums[p2]:
p2 = mid
剑指offer:旋转数组的最小数字
问题描述:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
解法:最小元素正好是两个子数组的分界线,可以用二分查找法
时间复杂度:O(logn)
# -*- coding:utf-8 -*-
class Solution:
def minNumberInRotateArray(self, rotateArray):
if len(rotateArray) <= 0:
return 0
p1 = 0
p2 = len(rotateArray) - 1
# 没有旋转
if rotateArray[p1] < rotateArray[p2]:
return rotateArray[p1]
# 如果rotateArray[p1] < rotateArray[p2] 说明是递增数列
while rotateArray[p1] >= rotateArray[p2]:
if p1 == p2 - 1:
return rotateArray[p2]
mid = (p1 + p2) // 2
if rotateArray[mid] == rotateArray[p1] and rotateArray[mid] == rotateArray[p2]:
res = rotateArray[p1]
for i in range(p1, p2+1):
if rotateArray[i] < res:
res = rotateArray[i]
return res
# mid 为左子数组的递增
if rotateArray[mid] >= rotateArray[p1]:
p1 = mid
# mid 为右子数组的递增
elif rotateArray[mid] <= rotateArray[p2]:
p2 = mid
剑指offer:数组中的逆序对
问题描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
解法:归并排序。左子数组的逆序对 + 右子数组的逆序对 + 合并的逆序对,合并的时候把较大的数字从后往前复制到辅助数组中,辅助数组的元素递增
时间复杂度:O(nlogn)
class Solution:
def merge_sort(self, data, copy, start, end):
if start == end:
copy[start] = data[start]
return 0
mid = (end - start) // 2
# 拆分成两个数组
# copy, data 交换
left = self.merge_sort(copy, data, start, start + mid)
right = self.merge_sort(copy, data, start + mid + 1, end)
# 两个子数组的合并
count = 0
# 前一个数组的末尾下标
i = start + mid
# 后一个数组的末尾下标
j = end
# 下标:辅助数组从后往前添加较大的元素
t_index = end
while i >= start and j >= (start + mid + 1):
# 左比右大,说明产生多个逆序对,个数为右边余下的数字个数
if data[i] > data[j]:
copy[t_index] = data[i]
count += (j - start - mid)
i -= 1
t_index -= 1
# 右比左大,没有逆序对
else:
copy[t_index] = data[j]
j -= 1
t_index -= 1
# 左数组有剩余
while i >= start:
copy[t_index] = data[i]
t_index -= 1
i -= 1
# 右数组有剩余
while j >= (start + mid + 1):
copy[t_index] = data[j]
t_index -= 1
j -= 1
# print(copy == data)
# print('data = ', data)
# print('copy = ', copy)
# 左子数组的逆序对 + 右子数组的逆序对 + 合并的逆序对
return left + right + count
def InversePairs(self, data):
if not data:
return 0
# 辅助数组的元素递增
copy = [i for i in data]
# 调用递归
count = self.merge_sort(data, copy, 0, len(data) - 1)
return count % 1000000007
leetcode:35. 搜索插入位置
问题描述:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
解法:二分查找
时间复杂度:O(logn)
class Solution(object):
def searchInsert(self, nums, target):
if not nums:
return 0
left = 0
# 如果target比nums里所有的数都大,则最后一个数的索引 + 1 就是候选值,因此,右边界应该是数组的长度
right = len(nums)
# 退出循环时,left == right
while left < right:
# 选左中位数,防止 left + right 导致整型溢出
mid = (left + right) >> 1
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
leetcode:378. 有序矩阵中第K小的元素
问题描述:给定一个n x n矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素。注意,它是排序后的第k小元素。
解法:二分查找法,根据题目可得左上角元素最小,右下角元素最大,计算中间值。然后计算小于等于目标值的元素个数,根据递增规则,从右上角开始查找,类似于题目“二维数组的查找”
时间复杂度:O(nlogk) ,k=最大值-最小值
class Solution(object):
def kthSmallest(self, matrix, k):
# 计算小于等于目标值的元素个数,根据递增规则,从右上角开始查找
def count_num(m, target):
i = 0
j = len(m) - 1
ans = 0
while i < len(m) and j >= 0:
if m[i][j] <= target:
ans += j + 1
i += 1
else:
j -= 1
return ans
# 思路:左上角元素最小,右下角元素最大,计算小于等于中间值的元素个数
left = matrix[0][0]
right = matrix[-1][-1]
# 二分法查找
while left < right:
mid = (left + right) >> 1
# print(' mid = ', mid)
count = count_num(matrix, mid)
# print('count = ', count)
if count < k:
left = mid + 1
else:
right = mid
return left