找实习,阿里一面遇到手写快排,写出来感觉没错(VS2013能通过),但在阿里的测试平台上运行未通过。细思极恐,赶紧总结一波。有幸看到SteveWang的两篇博客:排序算法总结(1)与排序算法总结(2)以及基数排序、计数排序与桶排序,总结的相当详细,我这里算是重新拜读一遍,结合自己的理解写下来。
一、排序算法分类,稳定性分析,时间复杂度与空间复杂度总结表
我们通常所说的排序算法往往指的是内部排序算法,即数据记录在内存中进行排序。而外部排序是指大文件的排序,即待排序的记录存储在外存储器上,在排序过程中需要进行多次的内、外存之间的交换。
(一)内部排序算法大体可分为两大类:
1、比较排序,平均时间复杂度范围:O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
然而,参考博客:https://blog.csdn.net/zlele0326/article/details/51281578,比较排序算法可以继续分为下面几类:
(1)插入排序:直接插入排序,二分法插入排序,希尔排序;
(2)选择排序:简单选择排序,堆排序;
(3)交换排序:冒泡排序,快速排序;
(4)归并排序;
2、非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。详见地址:戳我。
(二)稳定性分析
排序算法稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。
对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
例如,对于冒泡排序,原本是稳定的排序算法,如果将记录交换的条件改成A[i] >= A[i + 1],则两个相等的记录就会交换位置,从而变成不稳定的排序算法。
其次,说一下排序算法稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的。
(三)排序算法总结表
尽管记了很多次几个排序算法的时间复杂度与空间复杂度,日子一长又忘了,心累,各种排序的稳定性,时间复杂度、空间复杂度、稳定性总结如下图(参考:https://blog.csdn.net/foreverling/article/details/43798223):
图1
图2
网上现主要有两个版本的排序表格,如上图1,图2(来自《大话数据结构》)所示。可以看到两张图中希尔排序的时间复杂度不同,以及快速排序的空间复杂度不同,那究竟是什么?
(1)希尔排序时间复杂度分析:(摘自百度百科),希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。希尔排序的时间复杂度与增量序列的选取有关,例如希尔增量时间复杂度为O(n^2),而Hibbard增量的希尔排序时间复杂度为O(n^3/2),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择,但是比O( n^2 )复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 希尔排序算法的性能与所选取的分组长度序列有很大关系。只对特定的待排序记录序列,可以准确地估算关键词的比较次数和对象移动次数。想要弄清关键词比较次数和记录移动次数与增量选择之间的关系,并给出完整的数学分析,至今仍然是数学难题。
(2)快速排序空间复杂度分析:详见博客(https://blog.csdn.net/yuzhihui_no1/article/details/44198701),首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据,即快速排序的空间复杂度为:O(logn) ~O( n ) 。
最优的情况下空间复杂度为:O(logn) ;每一次都平分数组的情况
最差的情况下空间复杂度为:O( n ) ;退化为冒泡排序的情况
二、比较排序
1、直接插入排序
插入排序是一种简单直观的排序算法。它的工作原理非常类似于我们抓扑克牌
对于未排序数据(右手抓到的牌),在已排序序列(左手已经排好序的手牌)中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
具体算法描述如下:
(1) 从第一个元素开始,该元素可以认为已经被排序;
(2) 取出下一个元素,在已经排序的元素序列中从后向前扫描;
(3) 如果该元素(已排序)大于新元素,将该元素移到下一位置;
(4) 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
(5) 将新元素插入到该位置后;
(6) 重复步骤2~5;
C++代码为:
#include
#include
using namespace std;
// 分类 ------------- 内部比较排序
// 数据结构 ---------- 向量
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定
vector InsertionSort(vector A)
{
for (int i = 1; i < A.size(); i++) // 类似抓扑克牌排序
{
int get = A[i]; // 右手抓到一张扑克牌
int j = i - 1; // 拿在左手上的牌总是排序好的
while (j >= 0 && A[j] > get) // 将抓到的牌与手牌从右向左进行比较
{
A[j + 1] = A[j]; // 如果该手牌比抓到的牌大,就将其右移
j--;
}
A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
}
return A;
}
int main()
{
//输入不定长的待排序列
vector vec;
int temp;
char t;
cout << "输入待排序列为:";
do{
cin >> temp;
vec.push_back(temp);
} while ((t=cin.get())!='\n');
/////////////////////////////////////////////////////////////////////////////////////
int n = vec.size();//待排序列的长度 ,若输入为数组A,则int n = sizeof(A) / sizeof(int);
cout << "排序前的结果为:";
for (int i = 0; i < n; i++)
{
cout << vec[i] << " ";
}
cout << endl;
/////////////////////////////////////////////////////////////////////////////////////
vector res = InsertionSort(vec); //调用直接插入排序算法
cout << "排序后的结果为:";
for (int i = 0; i < n; i++)
{
cout << res[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
运行结果:
插入排序过程:动态图来自http://www.cnblogs.com/eniac12/p/5329396.html#3972917
插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,比如量级小于千,那么插入排序还是一个不错的选择。 插入排序在工业级库中也有着广泛的应用,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少量元素的排序(通常为8个或以下)。
python代码如下:
def InsertSort(myList):
length = len(myList)
for i in range(1,length):
# 设置当前值前一个元素的标识
j = i - 1
#如果当前值小于前一个元素,则将当前值作为一个临时变量存储,将前一个元素后移一位
if(myList[i] < myList[j]):
temp = myList[i]
myList[i] = myList[j]
# 继续往前寻找,如果有比临时变量大的数字,则后移一位,直到找到比临时变量小的元素或者达到列表第一个元素
j = j-1
while j>=0 and myList[j] > temp:
myList[j+1] = myList[j]
j = j-1
# 将临时变量赋值给合适位置
myList[j+1] = temp
if __name__=="__main__":
arr = [int(i) for i in input().split()] # 输入待排序列
InsertSort(arr) # 插入排序
print(" ".join(str(i) for i in arr)) # 输出排序结果
运行结果:
2、快速排序算法
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个元素要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治策略把一个序列分为两个子序列。步骤为:
(1)从序列中挑出一个元素,作为"基准"(pivot).
(2)把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
(3)对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
C++代码:
#include
#include
#include
using namespace std;
// 分类 ------------ 内部比较排序
// 数据结构 --------- 向量
// 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
// 稳定性 ---------- 不稳定
void Swap(vector& data, int i, int j)
{
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
int RandomInRange(int min, int max) //在min~max中随机生成一个值
{
int random = rand() % (max - min + 1) + min;
return random;
}
int Partition(vector& data, int start, int end) //Partition函数
{
if (data.size() <= 0 || start < 0 || end >= data.size())
throw new std::exception("Invalid Parameters");
//int index = RandomInRange(start, end); //可以不调用RandomInRange函数
//Swap(data, index, end);
int small = start - 1;
for (int index = start; index < end; ++index)
{
if (data[index] < data[end])
{
++small;
if (small != index)
Swap(data, index, small);
}
}
++small;
Swap(data, small, end);
return small;
}
//递归快速排序
void QuickSort(vector& data, int start, int end)
{
if (start >= end)
return;
int index = Partition(data, start, end); //基准的索引
QuickSort(data, start, index - 1);
QuickSort(data, index + 1, end);
}
int main()
{
//输入不定长的待排序列
vector vec;
int temp;
char t;
cout << "输入待排序列为:";
do{
cin >> temp;
vec.push_back(temp);
} while ((t = cin.get()) != '\n');
/////////////////////////////////////////////////////////////////////////////////////
int n = vec.size();//待排序列的长度 ,若输入为数组A,则int n = sizeof(A) / sizeof(int);
cout << "排序前的结果为:";
for (int i = 0; i < n; i++)
{
cout << vec[i] << " ";
}
cout << endl;
/////////////////////////////////////////////////////////////////////////////////////
QuickSort(vec, 0, n - 1); //调用快速排序算法
/////////////////////////////////////////////////////////////////////////////////////
cout << "排序后的结果为:";
for (int i = 0; i < n; i++)
{
cout << vec[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
运行结果:
快速排序过程展示:
快速排序是不稳定的排序算法,不稳定发生在基准元素与A[small+1]交换的时刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。
Java系统提供的Arrays.sort函数。对于基础类型,底层使用快速排序。对于非基础类型,底层使用归并排序。请问是为什么?
答:这是考虑到排序算法的稳定性。对于基础类型,相同值是无差别的,排序前后相同值的相对位置并不重要,所以选择更为高效的快速排序,尽管它是不稳定的排序算法;而对于非基础类型,排序前后相等实例的相对位置不宜改变,所以选择稳定的归并排序。
需要强调的是,很多公司的面试官喜欢在面试环节要求应聘者写出快速排序的代码,剑指offer上一个比较好的方法,保存了数组的两个位置index向前遍历数组,small用于保存交换的小于轴值的数,找到一个前移一步,即上面的函数:
void Swap(vector
&data, int i, int j) { int temp = data[i]; data[i] = data[j]; data[j] = temp; } int RandomInRange(int min, int max) //在min~max中随机生成一个值 { int random = rand() % (max - min + 1) + min; return random; } int Partition(vector &data, int start, int end) //Partition函数 { if (data.size() <= 0 || start < 0 || end >= data.size()) throw new std::exception("Invalid Parameters"); //int index = RandomInRange(start, end); //Swap(data, index, end); int small = start - 1; for (int index = start; index < end; ++index) { if (data[index] < data[end]) { ++small; if (small != index) Swap(data, index, small); } } ++small; Swap(data, small, end); return small; } //递归快速排序 void QuickSort(vector &data, int start, int end) { if (start >= end) return; int index = Partition(data, start, end); //基准的索引 QuickSort(data, start, index - 1); QuickSort(data, index + 1, end); }
现在回答,为什么在阿里笔试过程中,自己写出来的快排,感觉没错,但是运行结果不对。主要原因:void QuickSort(vector
& data, int start, int end) //快速排序
int Partition(vector& data, int start, int end) //Partition函数
写成了
void QuickSort(vector data, int start, int end) //快速排序
int Partition(vector data, int start, int end) //Partition函数
运行结果:
本质原因为:vector的传参方式理解不透彻,详见https://www.cnblogs.com/xiaoxi666/p/6843211.html。
为了在面试过程中快速写出代码,如果允许写python代码,则为:
# 快速排序
def quick_sort(array, l, r):
if l < r:
q = partition(array, l, r)
quick_sort(array, l, q - 1)
quick_sort(array, q + 1, r)
def partition(array, l, r):
x = array[r]
i = l - 1
for j in range(l, r):
if array[j] <= x:
i += 1
array[i], array[j] = array[j], array[i]
array[i + 1], array[r] = array[r], array[i + 1]
return i + 1
if __name__=="__main__":
arr = [int(i) for i in input().split()] # 输入待排序列
quick_sort(arr,0,len(arr)-1) # 快速排序
print(" ".join(str(i) for i in arr)) # 输出排序结果