名称 | 最优时间复杂度 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 | 稳定性 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | YES |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | NO |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | YES |
归并排序 | O(nlgn) | O(nlgn) | O(nlgn) | O(n) | YES |
快速排序 | O(nlgn) | O(nlgn) | O(n^2) | O(lgn)~O(n) | NO |
堆排序 | O(nlgn) | O(nlgn) | O(nlgn) | O(1) | NO |
希尔排序 | O(n^1.3) | O(nlgn) | O(n^2) | O(1) | NO |
计数排序 基数排序 | O(n) | O(m) | YES |
一、冒泡排序
对于一个int数组,请编写一个冒泡排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
冒泡排序:
时间复杂度 O(N^2) 空间复杂度O(1)
过程:
一开始交换的区间是0~N-1,即数组的整体。第1个数与第2个数比较,大的放在后面;第2个数与第3个数比较,大的放在后面;这样依次交换过去,最终最大的数会放在数组的后面。
然后把交换范围从0~N-1变为0~N-2,这样第二大的数在交换过后会放在倒数第二的位置。
依次进行这样的过程。直到交换范围只剩下一个数时,整个数组就变得有序了。
class BubbleSort {
public:
int* bubbleSort(int* A, int n) {
if (n<=1)
return A;
for (int j=0; j<n; j++){
for (int i=n-1; i>j; i--){
if (A[i]<A[i-1])
swap(A[i],A[i-1]);
}
}
return A;
}
};
二、选择排序
对于一个int数组,请编写一个选择排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
选择排序:
时间复杂度 O(N^2) 空间复杂度O(1)
过程:
一开始在0~N-1范围上选出最小值,把它放在位置0上,然后在1~N-1范围上选出最小值,把它放在位置1上。直到最后选择范围只包含一个数的时候,整个数组就变得有序了。
class SelectionSort {
public:
int* selectionSort(int* A, int n) {
if(n<=1) return A;
for (int i=0; i<n; ++i){
int smallest = i;
for (int j=n-1; j>i; --j){
if(A[j]<A[smallest]) smallest = j;
}
if(i!=smallest) swap(A[i],A[smallest]);
}
return A;
}
};
三、插入排序
对于一个int数组,请编写一个插入排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
插入排序:
时间复杂度 O(N^2) 空间复杂度O(1)
过程:
首先位置1上的数与位置0上的数进行比较,如果位置1上的数更小,则它与位置0上的数交换;接下来考察位置2上的数(记为a),如果a比位置1上的数更小,则a与位置1上的数交换,交换之后a再与位置0上的数比较,如果a更小,则a与之交换;对位置k上的数(记为b),b依次和前面位置上的数进行比较,如果b一直小于它前面的数,就一直进行这样的交换,直到b前面的数小于等于b,那么b就插入当前位置。
对1位置到N-1位置的数都进行这样的插入过程,最终整个数组变得有序。
class InsertionSort {
public:
int* insertionSort(int* A, int n) {
if(n<=1) return A;
for(int i=1; i<n; i++){
int key = A[i];
int j = i-1;
while(j>=0 && A[j]>key){
A[j+1] = A[j];
j--;
}
A[j+1] = key;
}
return A;
}
};
四、归并排序
对于一个int数组,请编写一个归并排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
归并排序:
时间复杂度 O(N logN) 空间复杂度O(N)
过程:
首先让数组中的每一个数单独成为长度为1的有序区间,然后把相邻的长度为1的有序区间进行合并,得到最大长度为2的有序区间,接下来再把相邻的有序区间合并,得到最大长度为4的有序区间,依次进行这样的合并过程,直至数组中的所有数都在一个有序区间,整个数组变得有序。
class MergeSort {
public:
int* mergeSort(int *A,int n){
mergeSort1(A,0,n-1);
return A;
}
void mergeSort1(int *A,int l,int r){
if(l>=r) return;
int mid=(l+r)/2;
mergeSort1(A,l,mid);
mergeSort1(A,mid+1,r);
merge(A,l,mid,r);
}
void merge(int *A,int l,int mid,int r){
int L[mid-l+2];
int R[r-mid+1];
for(int i=0;i<mid-l+1;i++){
L[i] = A[i+l];
}
for(int i=0;i<r-mid;i++){
R[i] = A[i+mid+1];
}
L[mid-l+1] = INT_MAX;
R[r-mid] = INT_MAX;
for(int i=0,j=0,k=l;k<=r;k++){
if(L[i]<R[j]) A[k]=L[i++];
else A[k]=R[j++];
}
}
};
五、快速排序
对于一个int数组,请编写一个快速排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
快速排序:
时间复杂度 O(N logN) 空间复杂度O(logN)~O(N) (取决于划分情况)
过程:
随机在数组中选一个数,小于等于它的数统一放在这个数的左边,大于它的数统一放在这个数的右边,接下来对于左右两个部分分别递归的调用快速排序的过程,这样就使得整个数组变得有序。
快速排序的划分(Partition)过程:
首先令划分值放在整个数组的最后位置,然后设计一个小于等于区间,初始时,小于等于区间长度为0,放在整个数组的左边。接下来从左到右遍历所有元素,如果当前元素大于划分值,则继续遍历下一个元素,如果当前元素小于等于划分值,则把当前数和小于等于区间的下一个数进行交换,然后令小于等于区间向右扩一个位置,直到遍历到最后一个数(也就是划分值),将它与小于等于区间的下一个数进行交换。
划分过程的时间复杂度为O(N)
class QuickSort {
public:
int* quickSort(int* A, int n) {
return qSort(A,0,n-1);
}
int* qSort(int* A, int p, int r){
if(p<r){
int q = partition(A,p,r);
qSort(A,p,q-1);
qSort(A,q+1,r);
}
return A;
}
int partition(int* A, int p, int r){
int x = A[r];
int i = p-1;
for(int j=p;j<r;j++){
if(A[j]<x){
i++;
swap(A[i],A[j]); //A[p..i]<x, A[i+1..r-1]>=x
}
}
swap(A[i+1],A[r]);
return i+1;
}
};
六、堆排序
对于一个int数组,请编写一个堆排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
堆排序:
时间复杂度 O(N logN) 空间复杂度O(1)
过程:
首先把数组中的N个数建成大小为N的大根堆,堆顶是整个数组的最大值,把堆顶元素和堆的最后一个元素进行交换,然后把最大值脱离出整个堆结构,放在数组的最后位置,作为数组的有序部分存在下来;接下来把N-1大小的堆从堆顶位置开始进行大根堆的调整,调整出N-1个数中的最大值放在堆顶位置,把堆顶位置的值与整个堆最后位置的值交换,最后位置脱离堆结构,放在数组的有序部分……当堆的大小减为1时整个数组变得有序。
class HeapSort {
public:
int* heapSort(int* A, int n) {
int heapSize = n-1;
buildMaxHeap(A,heapSize);
for(int i=n-1;i>0;i--){
swap(A[0],A[i]);
heapSize --;
maxHeapify(A,heapSize,0);
}
return A;
}
int* buildMaxHeap(int* A, int heapSize){
for(int i=heapSize/2;i>=0;i--){
maxHeapify(A,heapSize,i);
}
return A;
}
int* maxHeapify(int* A, int heapSize, int i){
int l = 2*i+1;
int r = 2*i+2;
int largest = i;
if(l<=heapSize && A[l]>A[i]) largest = l;
if(r<=heapSize && A[r]>A[largest]) largest = r;
if(i!=largest){
swap(A[i],A[largest]);
maxHeapify(A,heapSize,largest);
}
return A;
}
};
七、希尔排序
对于一个int数组,请编写一个希尔排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。保证元素小于等于2000。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
希尔排序:
时间复杂度 O(N logN) 空间复杂度O(1)
过程:
希尔排序是插入排序的改良算法。插入排序步长为1,希尔排序的步长是逐渐从大到小调整的。希尔排序的关键是步长的选择。
例: 6 5 3 1 8 7 2 4 步长为3
-> 1 4 3 2 5 7 6 8 -> 步长为2的调整 -> 步长为1的调整(插入排序) -> 1 2 3 4 5 6 7 8
class ShellSort {
public:
int* shellSort(int* A, int n) {
for(int s=n/2;s>=1;s/=2){
insertionSort(A,n,s);
}
return A;
}
void insertionSort(int* A, int n, int stride){
if(n<=1) return;
for(int j = stride; j<n;j++){
int key = A[j];
while(j-stride>=0 && key<A[j-stride]){
for(int i=0;i<=stride-1;i++){
A[j-i]=A[j-i-1];
}
j -= stride;
A[j]=key;
}
}
}
};
八、计数排序
对于一个int数组,请编写一个计数排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
计数排序:
时间复杂度 O(N) 空间复杂度O(M)(M为桶的数量)
时间复杂度为 O(N) 的排序算法,不是基于比较的排序算法,思想来自于桶排序思想。
过程:
身高100~300,建立100~300号桶,身高依次进入相应的桶,100~300号桶依次倒出身高,从桶里倒出来的身高就是有序的了。
class CountingSort {
public:
int* countingSort(int* A, int n) {
if(n<=1) return A;
int max = INT_MIN;
int min = INT_MAX;
for(int i=0;i<n;i++){
if(A[i]>max) max=A[i];
if(A[i]<min) min=A[i];
}
int len = max-min+1;
vector<vector<int>> res(len);
for(int i=0;i<n;i++){
res[A[i]-min].push_back(A[i]);
}
int cnt = 0;
for(int i=0;i<len;i++){
if(!res[i].empty()){
for(vector<int>::iterator itr=res[i].begin();itr!=res[i].end();itr++){
A[cnt++] = *itr;
}
}
}
return A;
}
};
九、基数排序
对于一个int数组,请编写一个基数排序算法,对数组元素排序。
给定一个int数组A及数组的大小n,请返回排序后的数组。保证元素均小于等于2000。
测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
基数排序:
时间复杂度 O(N) 空间复杂度O(M)(M为桶的数量)
时间复杂度为 O(N) 的排序算法,不是基于比较的排序算法,思想来自于桶排序思想。
过程:
首先假设被排序的数都是十进制的,然后准备0~9号桶,把每个数根据它个位数选择它进入几号桶,所有数进桶之后从0-9号桶依次倒出所有数,再依次按照十位数进桶,再依次倒出,。。。,再依次按照最高位数进桶,再倒出,倒出的序列就是排序之后的序列。
class RadixSort {
public:
int* radixSort(int* A, int n) {
if(n<=1) return A;
for(int i=0;i<=3;i++){
distribute(A,n,i);
}
return A;
}
void distribute(int* A, int n, int times){
queue<int> que[10];
for(int i=0;i<n;i++){
int num=A[i]/pow(10,times);
que[num%10].push(A[i]);
}
int idx = 0;
for(int i=0;i<10;i++){
while(!que[i].empty()){
A[idx]=que[i].front();
que[i].pop();
idx++;
}
}
}
};
十、小范围排序问题
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
给定一个int数组A,同时给定A的大小n和题意中的k,请返回排序后的数组。
测试样例:
[2,1,4,3,6,5,8,7,10,9],10,2
返回:[1,2,3,4,5,6,7,8,9,10]
【分析】
1、时间复杂度为O(n)的排序算法(计数排序、基数排序),不急于比较的排序算法的限制:不适用所有情况。不考虑。
2、时间复杂度为O(n^2)的排序算法,冒泡排序、选择排序:这两个排序算法与数组原始序列无关。插入排序的过程与原始顺序有关,每个元素移动距离不超过k,对本题来说,时间复杂度O(nk)。
3、时间复杂度为O(nlgn)的排序算法,快排、归并排序与数组原始序列无关。
答案:改进后的堆排序。最小值在a[0..k-1]中,将a[0..k-1]建立小根堆,堆顶是最小值,弹出堆顶,放在数组0位置,将a[k]放在小根堆堆顶,调整小根堆,弹出堆顶,放在数组1位置,……。每次调整的代价是O(lgk),总共n个数,整体时间复杂度O(nlgk)。
class ScaleSort {
public:
vector<int> sortElement(vector<int> A, int n, int k) {
// 边界检测
if(A.size()==0 || n<1 || k<0|| k>n) return A;
//初始化小顶堆
vector<int> minHeap(k);
for(int i=0;i<k;i++){
minHeap[i] = A[i];
}
for(int i=k/2;i>=0;i--){
minHeapify(minHeap,i,k);
}
//使用小顶堆,求出前n-k个值
for(int i=0;i<n-k;i++){
A[i] = minHeap[0];
minHeap[0] = A[i+k];
minHeapify(minHeap,0,k);
}
//对剩余的k个值,使用堆排序
for(int i=n-k;i<n;i++){
A[i] = minHeap[0];
swap(minHeap[0],minHeap[k-1]);
minHeapify(minHeap,0,--k);
}
return A;
}
void minHeapify(vector<int> &A, int i, int heapSize){
int l = 2*i+1;
int r = 2*i+2;
int min=i;
if(l<heapSize && A[l]<A[i]) min = l;
if(r<heapSize && A[r]<A[min]) min = r;
if(i!=min){
swap(A[i],A[min]);
minHeapify(A,min,heapSize);
}
}
};
十一、重复值判断问题
请设计一个高效算法,判断数组中是否有重复值。必须保证额外空间复杂度为O(1)。
给定一个int数组A及它的大小n,请返回它是否有重复值。
测试样例:
[1,2,3,4,5,5,6],7
返回:true
【分析】
如果没有空间复杂度的限制,用哈希表实现。在哈希表遍历数组的过程中,统计每个数出现的次数,时间复杂度O(n)、空间复杂度O(n)。
加上空间复杂度为O(1)的限制,要先排序、再判断,遍历一遍就知道有没有重复值。
考察经典排序算法的空间复杂度限制——选择堆排序。
堆排序的经典实现使用了递归的方式(需要用调用栈实现递归过程,栈的大小为堆的层数),这样空间复杂度不是O(1)而是O(lgn)。因此本题需要非递归版本的堆排序。
class Checker {
public:
bool checkDuplicate(vector<int> a, int n) {
// write code here
heapSort(a,n);
for(int i=1;i<=n-1;i++){
if(a[i]==a[i-1]) return true;
}
return false;
}
void heapSort(vector<int>& a, int n){
//build maxHeap from the last parent node
for(int p=(n-1)/2;p>=0;p--){
maxHeapify(a,n-1,p);
}
//heap sort
for(int end=n-1;end>=1;end--){
swap(a[0],a[end]);
maxHeapify(a,end-1,0);
}
}
void maxHeapify(vector<int>& a, int heapSize, int i){
int p=i;
int key=a[i];
int l=2*p+1;
int r=2*p+2;
while(l<=heapSize){
if(key>=a[l] && (r<=heapSize && key>=a[r])) break; //have right child, parent >= left child, donot need change
else if(key>=a[l] && r>heapSize) break; //no right child, parent > left child, donot need change
else if(r<=heapSize && a[r]>=a[l] && a[r]>key){ //have right child, right child is the largest, swap
swap(a[r],a[p]);
p=r;
l=2*p+1;
r=2*p+2;
}
else if(r<=heapSize && a[l]>a[r] && a[l]>key){ //have right child, left child is the largest, swap
swap(a[l],a[p]);
p=l;
l=2*p+1;
r=2*p+2;
}
else if(r>heapSize && a[l]>key){ //no right child, parent < left child, swap
swap(a[l],a[p]);
p=l;
l=2*p+1;
r=2*p+2;
}
}
}
};
十二、有序数组合并问题
有两个从小到大排序以后的数组A和B,其中A的末端有足够的缓冲空容纳B。请编写一个方法,将B合并入A并排序。
给定两个有序int数组A和B,A中的缓冲空用0填充,同时给定A和B的真实大小int n和int m,请返回合并后的数组。
要求合并后的数组是从小到大排序的。
【分析】要点在于,AB有效部分要从后至前比较(直至B完全拷贝进A),以免A的有用部分不会因合并过程而被覆盖掉。
class Merge {
public:
int* mergeAB(int* A, int* B, int n, int m) {
// write code here
int i = n-1;
int j = m-1;
for(int k=n+m-1;k>=0;k--){
A[k] = A[i]>=B[j]? A[i--] : B[j--];
}
return A;
}
};
十三、三色排序问题(荷兰国旗问题)
有一个只由0,1,2三种元素构成的整数数组,请使用交换、原地排序而不是使用计数进行排序。
给定一个只含0,1,2的整数数组A及它的大小,请返回排序后的数组。保证数组大小小于等于500。
测试样例:
[0,1,1,0,2,2],6
返回:[0,0,1,1,2,2]
【分析】本题主要过程与快排划分过程类似(0区、2区)。时间复杂度O(n),额外空间复杂度O(1)。
class ThreeColor {
public:
vector<int> sortThreeColor(vector<int> A, int n) {
// write code here
if(n<=1) return A;
int left=-1;
int right=n;
int idx=0;
//A[0..left]: 0
//A[right..n-1]: 2
while(idx<right){
if(A[idx]==0) swap(A[idx++],A[++left]);
else if(A[idx]==2) swap(A[idx],A[--right]); //note: idx not add 1
else idx++;
}
return A;
}
};
十四、有序矩阵查找问题
现在有一个行和列都排好序的矩阵,请设计一个高效算法,快速查找矩阵中是否含有值x。
给定一个int矩阵mat,同时给定矩阵大小nxm及待查找的数x,请返回一个bool值,代表矩阵中是否存在x。所有矩阵中数字及x均为int范围内整数。保证n和m均小于等于1000。
测试样例:
[[1,2,3],[4,5,6],[7,8,9]],3,3,10
返回:false
【分析】最优解时间复杂度O(n+m),额外空间复杂度O(1)。从右上开始找,如果当前数>目标数,向左移动;如果当前数<目标数,向下移动。
class Finder {
public:
bool findX(vector<vector<int> > mat, int n, int m, int x) {
int cx=m-1;
int cy=0;
while(cx>=0 && cy<=n-1){
if(mat[cy][cx]==x) return true;
else if(mat[cy][cx]<x) cy++;
else cx--;
}
return false;
}
};
十五、最短子数组问题
对于一个数组,请设计一个高效算法计算需要排序的最短子数组的长度。
给定一个int数组A和数组的大小n,请返回一个二元组,代表所求序列的长度。(原序列位置从0开始标号,若原序列有序,返回0)。保证A中元素均为正整数。
测试样例:
[1,4,6,5,9,10],6
返回:2
【分析】
最优解时间复杂度O(n),额外空间复杂度O(1)。
从左到右遍历,记录遍历过的部分的最大值,考察该最大值大于当前数的情况,只记录发生这种情况的最右位置。
从右到左遍历,记录遍历过的部分的最小值,考察该最小值小于当前数的情况,只记录发生这种情况的最左位置。
最左位置~最右位置,这个范围就是要排序的最短子数组。
class Subsequence {
public:
int shortestSubsequence(vector<int> A, int n) {
// write code here
if(n<=1) return 0;
int left_max=A[0];
int left_idx=0;
int right_min=A[n-1];
int right_idx=n-1;
for(int i=0;i<=n-1;i++){
if(A[i]>=left_max) left_max=A[i];
else left_idx=i;
}
for(int i=n-1;i>=0;i--){
if(A[i]<=right_min) right_min=A[i];
else right_idx=i;
}
return left_idx>right_idx ? left_idx-right_idx+1 : 0;
}
};
十六、相邻两数最大差值问题
有一个整形数组A,请设计一个复杂度为O(n)的算法,算出排序后相邻两数的最大差值。
给定一个int数组A和A的大小n,请返回最大的差值。保证数组元素多于1个。
测试样例:
[1,2,5,4,6],5
返回:2
【分析】最优解时间复杂度O(n),额外空间复杂度O(n)。思想来自桶排序。遍历数组,找到最大值与最小值,把[min,max)等量分成n个区间(n为数组长度),每个区间对应一个桶,最大值单独放在第n+1个桶中。n个数、n+1个桶,必然出现空桶。同一桶中元素的差值不会大于桶区间,空桶两侧相邻数的差值,必大于桶区间。所以不用考虑同一个桶的相邻数,只用考虑桶间的相邻数。
class Gap {
public:
int maxGap(vector<int> A, int n) {
if(n<=1) return 0;
int max=INT_MIN;
int min=INT_MAX;
for(int i=0;i<=n-1;i++){
max = max<A[i]? A[i] : max;
min = min>A[i]? A[i] : min;
}
if(max-min<=1) return max-min;
vector<bool> hasNum(n+1,false);
vector<int> maxs(n+1,INT_MIN);
vector<int> mins(n+1,INT_MAX);
int bid=0;
for(int i=0;i<=n-1;i++){
bid = int(double(A[i]-min)/double(max-min)*n);
maxs[bid] = maxs[bid]<A[i] ? A[i] : maxs[bid];
mins[bid] = mins[bid]>A[i] ? A[i] : mins[bid];
hasNum[bid] = true;
}
int firstbid=0;
int ans=0;
int lastMax=0;
for(int i=0;i<=n;i++){
if(hasNum[i]){
lastMax=maxs[i];
firstbid=i;
break;
}
}
for(int i=firstbid+1;i<=n;i++){
if(hasNum[i]){
if(mins[i]-lastMax > ans) ans = mins[i]-lastMax;
lastMax=maxs[i];
}
}
return ans;
}
};