排序算法
常用的排序算法主要有以下几种:
- 冒泡排序
- 插入排序
- 归并排序
- 快速排序
- 堆排序
- 桶排序
- 选择排序
- 希尔排序
- 基数排序
写一下前七种排序算法的原理及C++实现
冒泡排序
冒泡排序的平均及最坏情况时间复杂度均为O(n2),原理是遍历若干次要排序的数列,每次遍历时,它都会从前往后依次的比较相邻两个数的大小;如果前者比后者大,则交换它们的位置。C++实现如下:
using namespace std;
void Bubblesort(vector<int>& v){
int n = v.size();
for (int i=0; i<n; i++)
for (int j=n-1; j>i; j--){
if(v[j] < v[j-1] ){
swap(v[j], v[j-1]);
}
}
}
插入排序
对于少量元素的排序,插入排序是一个有效的算法,且该算法原址排序插入的数,在任何时候,最多只有其中的常数个数值存储在外面。插入算法的平均及最坏情况时间复杂度均为O(n2),代码如下:
using namespace std;
void InsertSort(vector<int>& v){
int n = v.size();
for (int i=1; i<n; i++){
int tmp = v[i];
int j=i-1;
for( ; j>=0 && v[j] > tmp; j--)
v[j+1] = v[j];
v[j+1] = tmp;
}
}
快速排序
快排使用了分治法,将数组V[p, …, r]分为两个(可能为空)的子数组V[p, …, q-1] 和 V[q, …, r],使得V[p, …, q-1] 的元素都小于等于V[q],而V[q]也小于等于A[q+1…r]中的每个元素。再递归调用快速排序,对子数组V[p, …, q-1] 和A[q+1…r]进行排序。快速排序的平均时间复杂度为O(nlogn),最坏情况时间复杂度为O(n2),代码如下:
using namespace std;
void QuickSort(vector<int>& v, int l, int r){
if (l >= r) {
return;
}
int povit = v[r];
int j = l-1;
for (int i=l; i<=r; i++) {
if(v[i] < povit){
j++;
swap(v[j], v[i]);
}
}
swap(v[j+1], v[r]);
QuickSort(v, l, j);
QuickSort(v, j+2, r);
}
任何一个元素都可以选来被当做pivot,但其合适与否与影响QuickSort的效率。为避免“poivt不够随机”带来的恶化效应,最理想的方式是取整个序列的头、尾、中三个位置的元素,以其中值作为povit。
归并排序
归并排序基于分治算法,首先分解待排序的n个元素的序列为两个各具n/2个元素的子序列,再使用归并排序递归地排序两个子序列,最后合并两个已排序的子序列成完整的已排序的序列。归并排序的平均及最坏情况时间复杂度均为O(nlogn),代码如下:
using namespace std;
//合并子序列
void Merge(vector<int>& v, int l, int q, int r){
int n1 = q - l + 1;
int n2 = r - q;
vector<int> v1(n1+1, INT_MAX);
vector<int> v2(n2+2, INT_MAX);
for (int i=0; i<n1; ++i) v1[i] = v[l+i];
for (int i=0; i<n2; ++i) v2[i] = v[q+i+1];
int i=0, j=0;
for (; l<=r; ++l) {
if(v1[i] < v2[j]){
v[l] = v1[i];
++i;
}
else {
v[l] = v2[j];
++j;
}
}
}
void MergeSort(vector<int>& v, int l, int r){
if (l < r){
int q = (l+r)/2;
MergeSort(v, l, q);
MergeSort(v, q+1, r);
Merge(v, l, q, r);
}
}
选择排序
选择排序的原理较为简单,即遍历未排序的数组,选择最小(或最大)的数放在已排序数组的末尾,时间复杂度为O(n2),代码如下:
void SelectSort(vector<int>& v){
int n = v.size();
for (int i=0; i<n; ++i){
int min = i;
for (int j=i; j<n; ++j) {
if(v[j] < v[min])
min = j;
}
swap(v[i], v[min]);
}
}
堆排序
堆排序的平均和最坏情况时间复杂度均为O(nlogn),且具有空间原址性。堆排序使用了“堆”这种数据结构进行信息管理。堆排序使用了最大堆,最大堆的性质是指出来根以外的所有节点i都要满足:
A
[
P
A
R
E
T
N
(
i
)
]
>
=
A
[
i
]
A[PARETN(i)] >= A[i]
A[PARETN(i)]>=A[i]
堆排序过程需要一个函数Max_Heapify来维护最大堆的性质,序列的最大值在最大堆的根节点,因此循环将根节点的值与为排序序列的最后一个值交换即可到达排序目的,代码如下:
void MaxHeapify(vector<int>& v, int i, int& n){
int l, r;
if (i == 0){
l=1, r=2;
} else {
l = 2*i + 1; //左孩子下标
r = 2*i + 2; //右孩子下标
}
int lagest;
if (l < n && v[l] > v[i]) lagest = l;
else lagest = i;
if (r < n && v[r] > v[lagest]) lagest = r;
if (lagest != i) {
swap(v[i], v[lagest]);
MaxHeapify(v, lagest, n);
}
}
void HeapSort(vector<int>& v){
int n=v.size();
for (int i=n/2 - 1; i>=0; --i)
MaxHeapify(v, i, n);
for (int i=n-1; i>0; --i){
swap(v[0], v[i]);
n--;
MaxHeapify(v, 0, n);
}
}
桶排序
在上述的几种算法中,排序都是依赖于各元素之间的比较,这类算法也可称为比较排序。任何比较排序算法在最坏情况下都要经过Ω(nlogn)次比较,因此归并排序和堆排序是渐进最优的。但是下面将要介绍的具有线性时间复杂度的桶排序不是用比较来确定排序的,因此,下界Ω(nlogn)对它是不适用的。
桶排序适用于输入数据服从均匀分布的排序,它的原理与哈希表有些类似,它将[0, 1)的区间划分为n(n为待排序数组长度)个相同大小的子区间,称为桶。然后将n个输入数风别放在各个桶中。应为输入数据是均匀的,所以一般不会出现很多数落在同一个桶中的情况。输出是先对每个桶中的数进行排序,然后遍历每个桶,按照次序把各个桶中的元素列出来即可。代码如下:
struct ListNode{
pair<int, int> val;
ListNode* next;
ListNode(int x, int y) : val(x, y), next(NULL){}
};
void BucketSort(vector<int>& v){
int n = v.size();
vector<ListNode*> bucket(n, NULL); //桶
int Max = 0;
for (int i=0; i<n; ++i){
Max = max(Max, v[i]);
}
Max++;
for (int i=0; i<n; ++i){
double val = static_cast<double> (v[i]) / static_cast<double> (Max);
int j = val * n ;
if(bucket[j] == NULL){
bucket[j] = new ListNode(0, 1);
ListNode* temp1 = new ListNode(v[i], Max);
bucket[j]->next = temp1;
} else {
ListNode* temp2 = new ListNode(v[i], Max);
ListNode* temp3 , *temp4;
temp3 = bucket[j];
temp4 = bucket[j]->next;
while (temp4){
double k = static_cast<double> (temp4->val.first) /
static_cast<double> (temp4->val.second);
if ( val < k ) {
break;
}
else {
temp3 = temp3->next;
temp4 = temp4->next;
}
}
if (temp4){
temp3->next = temp2;
temp2->next = temp4;
} else {
temp3->next = temp2;
}
}
}
for (int i=0; i<n; ++i){
if(bucket[i]) bucket[i] = bucket[i]->next;
}
int j=0;
for (int i=0; i<n; ++i){
while (bucket[i]){
v[j]=bucket[i]->val.first;
j++;
delete bucket[i];
bucket[i] = bucket[i]->next;
}
}
}
即使输入数据不服从均匀分布,只要输入数据满足下列性质:只要所有桶的大小的平方和与总的元素呈线性关系。那么桶排序依然可以在O(n)的时间内完成。
当输入数组的最大值不大时,可用以下方法:
void BucketSort_2(vector<int>& v){
int n=v.size();
int Max = 0;
for (int i=0; i<n; ++i){
Max = max(Max, v[i]);
}
vector<int> tmp(Max+1, 0);
for (int i=0; i<n; ++i) {
tmp[v[i]]++;
}
int j=0;
for (int i=0; i<Max+1; ++i) {
while (tmp[i] != 0) {
v[j] = i;
j++, tmp[i]--;
}
}
}
最后再写一下C++ STL里的sort函数,sort函数适用对象其实不多,因为关系型容器拥有自动排序功能,底层采用RB-Tree,所以不需要用到sort算法,序列式容器中的stack、queue和priority-queue都有特定的出入口,不允许用户对元素排序,所以还剩下vector和deque可以用sort函数。
上面讲了七种排序算法,sort函数用到了其中的插入排序,快速排序和堆排序。当输入序列长度n<=16时,采用插入排序,大于16则采用快速排序,当快排循环深度大于2logn时,改用堆排序。