一道快排题
Q:对一个已知长度的数组或列表,找出第k大的值。要求时间复杂度<=Nlog2N。
PS:作为一名一直使用并将长期使用python的程序猿而言,在不考虑时间复杂度和空间复杂度的情况下,第一反应应该是sorted排个序,然后取第k-1个值即可。天真!!!这么简单还是题吗!!!你以为坑只是时间复杂度?!?
一、切题
平均时间复杂度在Nlog2N级别的算法,应该有如下几个:快排、归并排和堆排。ps:快排是笔者昨天才再一次认真地过了一遍。归并还没看到。堆排在python是有built-in模块支持的。这次咱们先聊一聊快排。
其实在拿到这道题的时候,我的第一反应是遍历选出k个最小值,存放在另一个数组里,在存放的过程中有一个对该数组当前所有元素的遍历。这样便会存在一个隐性问题:1、最终需要而外k的空间复杂度;2、kN的时间复杂度。于是乎就有了k+kN与Nlog2N的大小比较了。
1、快排的多版本介绍
(1) 题主版
快排原理描述大致可以理解为:左小右大(左大右小)和分而治之。
即:1、选取基准数,把小于该数的数全丢左边,大于的都丢右边;2、递归调用该方法。
左丢右丢的思想就是寻找基准数左右符合条件的数(左边大于基准数,右边小于基准数)进行位置互调,直到两者下标重合。
于是:
在python里面的思想就可以是:1、任取数组里的一个数(这里我取的是向上取整的中间数),实现左丢右丢;对丢出来的左右列表进行如上递归操作,直到需要递归的列表长度为1(或者可以理解为索引一开始就交叉重合),那说明对一个基准数的遍历已经OK了
然后,重点就是递归了。函数要能做到递归,需要“上下兼容”。这个词是在一本编程杂志书看到的,可以这么理解:因为递归函数传入的参数,是需要该函数的输出或者中间输出做铺垫的,该函数的某一个过程的输出很可能就是该函数的输入。因此在设计函数的时候,需要尽可能保证函数传入的参数是灵活可扩展的。所以题主写了两个版本(当然第一个版本被淘汰了,因为扩展性极其差,甚至无法扩展)
于是乎,第一个版本,函数传入的参数是一个值,即数组“array”
def test(array):
print(array)
start = 0
end = len(array) - 1
index = (start + end) // 2
number = array[index]
while start <= end:
# 保证作比较的数必须大于左边
while array[start] < number:
start += 1
while array[end] > number:
end -= 1
if start > end:
continue
array[start], array[end] = array[end], array[start]
start += 1
end -= 1
print(array)
这一批代码是没办法向下兼容的。只能实现一次调用,要是想继续调用,至少得写一个扩展函数,写扩展函数的方法并非不行,而是没有必要,所以我们尝试换一种思路进行
OK,既然区间需要迭代,那就把迭代区间也写到函数里,应该可以避免迭代中进行迭代。
琢磨一下,慢慢造出第二版,也是最终版
def test(array, start, end):
# 设置作比较的数
index = (start + end) // 2
number = array[index]
# 设置遍历的索引位置
i, j = start, end
print('source:', array)
print('source:',i, j, index, '---', start, end, '---', array[index])
# 如果索引交叉了,说明对某一个数的对比已经完成一个轮回
while i <= j:
# 保证作比较的数必须大于左边
while array[i] < number:
i += 1
while array[j] > number:
j -= 1
if i > j:
continue
array[i], array[j] = array[j], array[i]
print('running:', array)
print(i, j, index, '---', start, end, '---', array[index])
i += 1
j -= 1
# 因为作比较的数左右的数存在不稳定性(即:可能左边或者右边的数普遍偏小,导致i或者j步长较大),因此递归的条件应该是整个区间
# 左递归
if start < j:
test(array, start, j)
if end > j:
test(array, i, end)
递归区间和条件也是比较绕的地方,琢磨了很久,递归的判断条件怎么写,到底递归的必要条件是啥。因为逻辑还是挺绕的,这个条件是在观察中间的输出的基础上推出来的。其实只要遍历的索引重合就说明不用再继续递归了,反过来就是不重合时需要递归,这个想法跟while循环一致。While循环也是主要考虑“什么时候调换位置”,运用的也是所谓的“反过来”的想法实现的。
(2) 填坑版
填坑版比较符合C系列语言的思想。大致意思就是,随机选取基准数(一般选取首个),先找小于基准数的值(右侧开始左探索),交换位置,记下位置;后找大于基准数的值(左侧开始右探索),交换位置。然后继续重复以上步骤。如果从大到小排序,就必须先左再右,取最后值做基准数(顺序和位置决定了先交换的值的条件)。这是填坑版的一些小小限制,详见:https://blog.csdn.net/code_ac/article/details/74158681
(3) 插入版
插入版的主要思想就是,把基准数先拿出来,把小于基准数的值放到该位置,大于基准数的值放到刚才拿出来的数的位置,最后把该基准数插入去大于基准数的值的位置,完成一次整体替换。跟填坑版是很类似很类似的。插入版需要保存两个临时变量,进行三次插入;填坑版需要保存一个临时变量,进行四次插入。详见https://www.cnblogs.com/landpack/p/4781579.html
以上后两种方法,在各大技术分享站有介绍,这里只是做个人的见解分析。
2、题目的最后处理
最后的问题
第K个就一定是第K大的????
第K个就一定是第K大的????
第K个就一定是第K大的????
在后来的题中,出题人给我出了第二题,如何清洗出一个不含重复元素的列表或数组。
直到我自己输出了第一个问题的答案以后,才想到,列表或数组元素是有可能重复的!!!
所以,第k个值不一定是第k大的值(说到这,下次把数据库的四大排序更加分享一下,其应用场景和这个题的k取值紧密相连(如rank和dense_rank))。因此我们需要去重再取第K大的值。
于是乎,怎么去重。今天早上我还问了一下我的同事,我说,给你一个列表,你怎么清洗出不重复的元素并找到第k大的值,问了俩。问了一个搞python,一个搞数据库的。玩数据库的说,先放到excel,插进数据库,然后排序选取index=k的。玩python的说,遍历list,插入另一个list2,插入条件是not in list2。我觉得都ok,都有很强的逻辑.个人觉得其实不重复的list它就是集合的概念,其实用python的set函数就OK。再不然其实用python的字典操作也是OK的,至少它底层也是一种哈希函数的应用,跟集合是异曲同工的。不讨论好坏,后两者本人更认为是pythonic的玩法。
所以。其实这道题,本人认为,应该有两种思路。一种是set后sort,另一种是先sort后set,两者的区别题主暂时无法做出优劣判断,单方面觉得,会存在一个界点,两者的效率在该界限有胜负变更的变化。如:
类似,仅仅是个人的经验。在python的多线程和多进程编程,就是存在这种边界效应的。
OK,整个排序题的核心代码如下:
import random
random.seed(0)
lis = []
for i in range(20):
lis.append(random.randint(0, 10))
random.shuffle(lis)
def test(array, start, end):
# 设置作比较的数
index = (start + end) // 2
number = array[index]
# 设置遍历的索引位置
i, j = start, end
print('source:', array)
print('source:',i, j, index, '---', start, end, '---', array[index])
# 如果索引交叉了,说明对某一个数的对比已经完成一个轮回
while i <= j:
# 保证作比较的数必须大于左边
while array[i] < number:
i += 1
while array[j] > number:
j -= 1
if i > j:
continue
array[i], array[j] = array[j], array[i]
print('running:', array)
print(i, j, index, '---', start, end, '---', array[index])
i += 1
j -= 1
# 因为作比较的数左右的数存在不稳定性(即:可能左边或者右边的数普遍偏小,导致i或者j步长较大),因此递归的条件应该是整个区间
# 左递归
if start < j:
test(array, start, j)
if end > j:
test(array, i, end)
然后因为早期教育中,这些排序算法都是C或C++写的,于是尝试用C也写了一个,如下:
#include <stdio.h>
void test(int array[], int start, int end);
int main() {
int array[10];
int i = 0;
srand(1);
for (i=0;i<10;i++){
array[i] = rand()%10;
}
for (i=0;i<10;i++){
printf("%d", array[i]);
}
test(array, 0, 9);
printf("\n--------------\n");
for (i=0;i<10;i++){
printf("%d", array[i]);
}
}
void test(int array[], int start, int end){
int index = (start + end) / 2;
int number = array[index];
int i, j;
i = start;
j = end;
int tmp;
while (i <= j){
while (array[i] < number){
i++;
}
while (array[j] > number){
j--;
}
if (i<=j){
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
i++;
j--;
}
}
if (start < j){
test(array, start, j);
}
if (end > i){
test(array, i, end);
}
}
最后,python版,不考虑复杂度的用法
import random
random.seed(0)
lis = []
for i in range(20):
lis.append(random.randint(0, 10))
random.shuffle(lis)
def test(array):
array_ = list(sorted(set(array)))[k]
print(array_)
人生苦短,我学python
以上为本次分享技术栈,如有不当,请指正