整个排序能够在主存中完成,元素个数相对较小(小于10^6),称为内部排序,若不能在主存中完成而必须在磁盘或磁带上完成的排序则为外部排序。
插入排序
由N-1趟排序组成,对于P = 1到P=N-1趟,插入顺序保证从位置0到P上的元素为已排序状态。
void insertSort(vector<int>& a)
{
int i, j;
for(i = 1; i < a.size(); i ++)
{
int temp = a[i];
for(j = i; j > 0 && a[j - 1] > temp; j --)
a[j] = a[j - 1];
a[j] = temp;
}
}
时间复杂度分析:最好情况,全部有序,需要比较n-1次,最好情况无交换,故时间复杂度为O(n),最坏情况为逆序输入时a[j] = a[j - 1]要执行2+3+4+……+N,时间复杂度为O(n^2),平均情况下也是O(n^2)。
希尔排序
冲破二次时间屏障的第一批算法,它通过比较相距一定间隔的元素工作,各趟比较距离随着算法进行而减少,直到只比较相邻元素的最后一趟排序为止,故希尔排序有时也叫做缩小增量排序。
希尔排序使用一个序列h1=1,h2,h3.....ht为增量序列,使用增量hk排序后后,对于每个i,a[i] <= a[i+hk]。流行的序列为shell建议的序列,ht = [N/2] 和 hk= [hk+1/2]
void shellSort(vector<int>& a)
{
int i, j, increment;
for(increment = a.size() / 2; increment > 0; increment /= 2)
{
for(i = increment; i < a.size(); i ++)
{
int temp = a[i];
for(j = i; j >= increment && a[j - increment] > temp; j -= increment)
a[j] = a[j - increment];
a[j] = temp;
}
}
}
使用shell增量时希尔排序的最坏情形运行时间为O(n^2), 若采用Hibbard增量(1,3,7,……,2 ^ (k - 1)),则最坏时间复杂度为O(n ^ 3/2)。Sedgewick提出的增量序列,最坏时间复杂度为O(n ^ 4/3)。
堆排序
建立大顶堆O(n),然后每次将堆顶元素与最后一个元素交换并调整堆,共执行n次为O(nlogn), 故时间复杂度为O(nlogn)。
void heapAdjust(vector<int>& a, int parent, int n);
{
int temp = a[parent];
while(2 * parent + 1 < n)
{
int child = 2 * parent + 1;
if(child + 1 < n && a[child + 1] > a[child])
child ++;
if(a[parent] < a[child]
{
a[parent] = a[child];
parent = child;
}
else
break;
}
a[parent] = temp;
}
void heapSort(vector<int>& a)
{
int n = a.size();
for(int i = (n - 1) / 2, i >= 0; i --)
heapAdjust(a, i, n);
for(int i = n - 1; i > 0; i --)
{
swap(a[0], a[i];
heapAdjust(A, 0, i);
}
}
归并排序
基本操作为为合并两个已经有序的表(分治)
void merge(vector<int>& a, vector<int>& temp, int begin, int mid, int end)
{
int i = begin, j = mid, k = begin;
while(i != mid && j != end + 1)
{
if(a[i] <= [j])
temp[k ++] = a[i ++];
else
temp[k ++] = a[j ++];
}
while(i != mid)
temp[k ++] = a[i ++];
while(j != end + 1)
temp[k ++] = a[j ++];
for(int i = begin, i <= end; i ++)
a[i] = temp[i];
}
void mSort(vector<int>& a, vector<int>& temp, int begin, int end)
{
if(begin < end)
{
int mid = left + (right - left) / 2;
mSort(a, temp, begin, mid);
mSort(a, temp, mid + 1, end);
merge(a, temp, begin, mid + 1, end);
}
}
void mergeSort(vector<int> a)
{
vector<int> temp(n);
msort(a, temp, 0, a.size() - 1);
}
时间复杂度分析:T(n) = 2T(n/2) + n ---T(n) = 2(2(T(n/4)) + n/2) + n = 4T(N/4) + 2N -----T(n) = 2^k*T(n/2^k) + k * n = nlog(n),虽然归并排序的运行时间为O(nlogn),是稳定排序,但它难用于主存排序,因为需要线性的附加空间O(n)。
快速排序
快速排序也是一种分治算法,与归并排序相同的是,递归地解决了两个子问题并需要线性的附加工作,不同在于两个子问题并不保证具有相等的大小,有潜在的隐患,但更快的原因在于分割在适当的位置并有效。
枢纽元的选取:(1)选第一个元素:对于预排序或反排序不利
(2)随机选取枢纽元:安全,但随机数生成昂贵
(3)三数中值分割法:选左端,右端及中间三个元素的中值,能消除预排序的最坏情形
对于等于枢纽元元素的处理:i, j都停止
对于小数组(N <= 20),快速排序不如插入排序好。
void quicksort(int* a)
{
quickS(a, 0, a.size() - 1);
}
#define cutoff 3
void quickS(int* a, int left, int right)
{
if (left + cutoff <= right)
{
int pivot = Median3(array, left, right);
int i = left, j = right - 1;
while (1)
{
while (a[++i] < pivot) {}
while (a[--j] > pivot) {}
if (i < j)
swap(a[i], array[j]);
else
break;
}
swap(array[i], array[right - 1]);
quickS(a, left, i - 1);
quickS(a, i + 1, right);
}
else
InsertionSort(a + left, right - left + 1); //对于小数组,插入排序更合适
}
int Median3(int*a, int left, int right)
{
int center = left + ( right - left) / 2;
if (a[left] > a[center])
swap(a[left], a[center]);
if (a[left] > a[right])
swap(a[left], array[right]);
if (a[center] > a[right])
swap(array[center], array[right]);
swap(array[center], array[right-1]);
return array[right - 1];
}
时间复杂度分析: T(N) = T(i) + T(N - i - 1) +cN
最坏情形为枢纽元始终为最小元素,i = 0,递推公式为T(N) = T(N - 1) +cN ,T(N - 1) = T(N - 2) +c(N - 1)...,累加后得到T(N) = T(1) + c(2 + 3 + ..._N) = O(N^2)
最好情形为枢纽元始终为中间元素,递推公式为T(N) = 2T(N - 1) +cN,与归并排序类似,O(NlogN)。
平均复杂度也为O(NlogN)。
桶式排序
以线性时间进行的排序,输入数据必须为小于M的正整数,则使用大小为M的count数组,初始化为0,称为M个桶,当读入ai时,count[ai] ++; 所有输入后,扫描count,打印排序后的表。算法的时间复杂度为O(N + M), 以空间换时间。
总结:
1.是否有大量重复元素?---- 三路快排(小于pivot,等于pivot,大于pivot)
2.是否大部分数据距离正确位置很近或者近乎有序----插入排序,快排的话(随机选pivot点)
3.是否数据的取值范围是不是非常有限?----计数排序(桶排序)
4.是否有其他要求?稳定?----归并排序
5.数据的存储情况,若为链表----归并排序(快排要求随机存储)
6.数据是否可以装载在内存中?----外部排序
top K问题partition实现:
int partition(vector<int>& input, int left , int right)
{
int pivot = right;
int start = left - 1, end = right;
while(1)
{
while(input[++ start] < input[pivot]){}
while(input[-- end] > input[pivot]){}
if(start < end)
swap(input[start], input[end]);
else
break;
}
swap(input[start], input[pivot]);
return start;
}
int findKthLargest(vector<int>& nums, int k) {
int left = 0, right = nums.size() - 1;
while (true) {
int pos = partition(nums, left, right);
if (pos == k - 1) return nums[pos];
if (pos > k - 1) right = pos - 1;
else left = pos + 1;
}
}