排序
参考五分钟学算法
复杂度比较
时间复杂度
- O(n2) 各种简单的排序:直接插入、直接选择、冒泡
- O(nlog2n) 快速排序、堆排序、归并排序
- O(n1+\\(\\lambda\\)),希尔排序
- 线性阶O(n)排序,基排序、桶、箱排序
稳定性
- 稳定排序:冒泡、插入、归并、基数排序
- 不稳定:选择、快速、希尔、堆排序
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。(相等元素相对位置不变)
选择排序
-
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
-
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
-
重复第二步,直到所有元素均排序完毕。
例题
面试题45. 把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
输入: [10,2]
输出: "102"
示例 2:
输入: [3,30,34,5,9]
输出: "3033459"
思路
将数组里数字当作字符串拼接起来,nm > mn那么就交换n与m,将m移动到n的前面。选择排序:
class Solution:
def minNumber(self, nums: List[int]) -> str:
nums = [str(i) for i in nums]
for i in range(len(nums)-1):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] > nums[j] + nums[i]:
nums[i],nums[j] = nums[j],nums[i]
return "".join(nums)
注意第一层循环到len(nums)-1
因为受到第二层循环i+1到 len(nums)
的限制。
快速排序
双向
两个指针i,j分别从左边和右边往中间扫,当nums[i]>基准
,nums[j]<基准
交换他们,最终可确定基准所在位置。这道题每次都舍弃一部分数组,将符合条件的放入buffer中,减少了运算,也是类似的思想。
例如:求前k个元素(由小到大)
def quick_select_without_optimizer(arr, k):
n = len(arr)
# 如果k大于n,没啥好说的,直接返回
if k >= n:
return arr
# 缓存
buffer = []
while arr:
# 选择最后一个元素作为标杆
mark = arr.pop()
less, greater = [], []
# 遍历数组,将元素分为less和greater
for x in arr:
if x <= mark:
less.append(x)
else:
greater.append(x)
# 判断三种情况,如果相等直接返回
if len(less) == k:
return less
# 如果小于,将less存入buffer,因为它一定是答案的一部分,可以简化计算
elif len(less) < k:
buffer += less
# k要减去less的长度
k -= len(less)
arr = [mark] + greater
else:
# 如果大于,直接舍弃右边
arr = less
单向
单向扫描,只有j从左到右扫描一遍,i指向小于基准区间的最右边,j指向大于基准区间的最左边,如上图。
public void qSort(int[] a,int begin,int end){
if(begin >= end) return;
int mid = partition(a, begin, end);
qSort(a, begin, mid - 1);
qSort(a, mid + 1, end);
}
public int partition(int[] a,int p ,int r){
int x = a[r];
int i = p - 1;
int j = p;
for(j = p; j < r; j++){
if(a[j] < x){
i++;
swap(a, i, j);
}
}
swap(a, i + 1, j);
return i + 1;
}
优化BFPRT算法
快排的问题在于,最坏情况(逆序)下时间复杂度为O(n^2)
。发生这种情况问题在于初始值的选择,也就是每次基准的选择。为了解决这种情况,引入BFPRT算法(Blum、Floyd、Pratt、Rivest、Tarjan五位大牛首字母)。
步骤
- 判断数组元素是否大于 5 ,如果小于 5 ,对它进行排序,并返回数组的中位数
- 如果元素大于 5 个,对数组进行分组,每 5 个元素分成一组,允许最后一个分组元素不足 5 个。
- 对于每个分组,对它进行插入排序
- 选择出每个分组排序之后的中位数,组成新的数组
- 重复以上操作
因此也叫中位数的中位数算法。具体复杂度分析参考,这个算法复杂度只有O(n)
,当然只是寻找基准,不是排序,算是快排的准备步骤。
def bfprt(arr, l=None, r=None):
if l is None or r is None:
l, r = 0, len(arr)
length = r - l
# 如果长度小于5,直接返回中位数
if length <= 5:
arr[l: r] = insert_sort(arr[l: r])
return l + length // 2
medium_num = l
start = l
# 否则每5个数分组
while start + 5 < r:
# 对每5个数进行插入排序
arr[start: start + 5] = insert_sort(arr[start: start + 5])
arr[medium_num], arr[start + 2] = arr[start + 2], arr[medium_num] #将所有的中位数都放在数组前面
medium_num += 1
start += 5
# 特殊处理最后不足5个的情况
if start < r:
arr[start:r] = insert_sort(arr[start:r])
_l = r - start
arr[medium_num], arr[start + _l // 2] = arr[start + _l // 2], arr[medium_num]
medium_num += 1
# 递归调用,对中位数继续求中位数
return bfprt(arr, l, medium_num)
之后再调用这个函数获取基准值即可,这只是避免快排最坏情况发生,选择基准的一种做法。接着差不多:
def quick_select(arr, k):
n = len(arr)
if k >= n:
return arr
mark = bfprt(arr) # 基准获取
arr[mark], arr[-1] = arr[-1], arr[mark]
buffer = []
while arr:
mark = arr.pop()
less, greater = [], []
for x in arr:
if x <= mark:
less.append(x)
else:
greater.append(x)
if len(less) == k:
return buffer + less
elif len(less) < k:
k -= len(less)
buffer += less
arr = [mark] + greater
else:
arr = less