排序分为内部排序和外部排序两种。内部排序是指在排序时先把待排数据都放入内存在进行排序;外部排序是在待排数据量很大,内存无法全部存入的情况下,需要访问外存的排序方法。
下面先来介绍内部排序:
内部排序主要有八大排序算法:冒泡排序,快速排序,直接插入排序,希尔排序,简单选择排序,堆排序,归并排序,基数排序。
以下讨论均默认为升序排序。
冒泡排序:第一次排序,从第一个元素到第n - 1个,依次比较与下一元素的大小,若比下一元素大则交换两元素位置。每次排序确定一个最大值。这样依次比较,第i次排序时,从第一个元素比较到第n - i 个。直到排序n - 1次结束。
冒泡排序的时间复杂度是O(n2),空间负责度是O(1).
算法实现如下:
void BubbleSort(int *a, int len){
int temp = 0;
for(int i = 0; i < len - 1; i++)
for(int j = 0; j < len - 1; j++){
if(a[j] > a[j + 1]){
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
快速排序:快速排序是冒泡的深度改进版,其思想是:首先,选择一个元素作为标准,遍历整个数组与其进行比较,把所有比它小的元素移动到其左边,比它大的元素移动到其右边。然后再分别对其左右两边的元素进行同样的操作,直到整个序列有序。
快速排序的时间复杂度是O(nlogn),空间复杂度是O(nlogn).
算法实现如下:
/**递归结束条件,序列中只有一个值即left == right时结束
每次循环,只要begin还没有等于end,则begin都是一个哨兵,且其值大于key,而且已经和end交换过。
判断条件有begin < end是为了防止begin和end错开导致begin > end,而且保证最后begin是和end重合的
重合点就是key的最终位置。
**/
void QuickSort(int *s, int low, int high){
if(low < high){
int key = s[low];
int begin = low;
int end = high;
while(begin < end){
while(s[end] > key && begin < end) end--;
s[begin] = s[end];
while(s[begin] < key && begin < end) begin++;
s[end] = s[begin];
}
s[begin] = key;
QuickSort(s, low, begin - 1);
QuickSort(s, begin+1, high);
}
}
直接插入排序:直接插入排序是,首先把序列的第一个元素当成有序的,每次排序都从后面序列中取出第一个元素并按顺序插入前面的有序序列中。循环执行,直到后面子序列为空,待排序列全部有序为止。
直接插入排序的时间复杂度是O(n2),空间复杂度是O(1).
其算法实现如下:
void InsertSort(int *s, int len){
for(int i = 1; i < len; i++){
int key = s[i];
int j = i - 1;
while(s[j] > key && j >= 0){
s[j + 1] = s[j];
j--;
}
s[j + 1] = key;
}
}
希尔排序:希尔排序是1959 年由D.L.Shell 提出来的,相对直接插入排序有较大的改进。希尔排序又叫缩小增量排序。
希尔排序的思想是:
- 为排序设置一个增量,按增量把待排序列分成几个子序列,相隔距离长度为增量整数倍的是一个子序列。
- 分别对每个子序列进行直接插入排序。
- 缩小增量,重复第1、2步。直到增量为1时,排序完成。
其算法实现如下:
void ShellSort(int *s, int len, int dk){
for(dk; dk > 0; dk = dk/2){
for(int i = 0; i < dk; i++){
for(int j = i + dk; j < len; j += dk){
int key = s[j];
int pre = j - dk;
while(s[pre] > key && pre >= 0){
s[pre + dk] = s[pre];
pre -= dk;
}
s[pre + dk] = key;
}
}
}
}
简单选择排序:简单选择排序的思想是,把待排序列看作两个部分,一个是已排有序部分,一个是待排无序部分。开始时,有序部分为空,整个待排序列都是无序部分。每次排序从待排无序部分选择一个最小值放入有序部分的最后,知道所有元素都进入有序部分,排序结束。
简单选择排序的时间复杂度是O(n2),空间复杂度是O(1).
其代码实现如下:
void SimpleSelectSort(int *s, int len){
for(int i = 0; i < len; i++){
int min = i;
for(int j = i + 1; j < len; j++){
if(s[j] < s[min]) min = j;
}
swap(s[i], s[min]);
}
}
堆排序:
在介绍堆排序之前,要首先介绍一下堆的概念。
堆是一个完全二叉树,其具有以下特点:
- 父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
- 每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为大根堆。当父结点的键值总是小于或等于任何一个子节点的键值时为小根堆。
堆排序的思想是利用堆的有序性,对待排序列进行排序。以大根堆为例,其思想是,
- 把整个待排序列看作一个待排序序列和一个已排序序列,把待排序序列整理为一个大根堆。(刚开始整个序列都是待排序序列)
- 每次把堆顶元素和堆尾元素交换,每次都是取堆的最大元素放到堆的末尾。则堆顶元素进入已排序序列。
- 然后把剩下的元素重新整理成大根堆,重复第二步。直到堆里只剩一个元素,则排序结束。
堆排序比简单选择排序的优越之处在于:虽然都是选择排序,但选择排序每次选择都要做O(n)次比较,而堆排序是通过堆进行选择,当大根堆建立之后,每次交换元素之后,堆的调整只需要O(logn)的时间复杂度,而简单选择需要O(n)次比较。
所以堆排序的时间复杂度是O(nlogn),空间复杂度是O(1).
其算法实现如下:
void AdjustHeap(int *s, int len){
for(int i = (len - 1)/2; i >= 0; i--){
int j = i; //正在调整的点
int max = j;
while(j*2 < len - 1){
if(s[2*j] > s[max]) max = 2*j;
if(s[2*j + 1] > s[max]) max = 2*j + 1;
if(max != j){
swap(s[max], s[i]);
j = max;
}
if(max == j) break;
}
}
}
void HeapSort(int *s, int len){
AdjustHeap(s, len);
for(int i = len -1; i > 0; i--){
swap(s[0], s[i]);
AdjustHeap(s, i);
}
}
归并排序:归并排序是首先把待排序列中每一个元素看成一个有序的序列,然后把相邻的有序序列成对进行归并排序,得到n/2个有序序列。再对他们进行相邻成对的归并排序,得到n/4个有序序列,如此反复归并,直到真个序列有序。
归并排序中,把长度为n的序列分成n份小序列需要logn步,每步的归并时间复杂度为O(n),所以整个排序的时间复杂度为O(nlogn),由于每次归并都需要一个数组存放相邻序列合并后的元素,在全局变量定义一个长度为n的数组,作为辅助空间。所以空间复杂度为O(n).
其算法的代码实现如下:
//int *s是待排序列,int *ns是辅助空间。两者长度相同
void Merge(int *s, int *ns, int begin, int mid, int end){
int first = begin;
int sec = mid + 1;
int i = begin;
while(first <= mid && sec <= end){
if(s[first] < s[sec]){
ns[i++] = s[first];
first++;
}
else{
ns[i++] = s[sec];
sec++;
}
}
if(first > mid){
for(sec; sec <=end; sec++)
ns[i++] = s[sec];
}
if(sec > end){
for(first; first <=mid; first++)
ns[i++] = s[first];
}
for(int j = begin; j <= end; j++){
s[j] = ns[j];
}
}
void MergeSort(int *s, int *ns, int begin, int end){
if(begin < end){
int mid = (begin + end)/2;
MergeSort(s, ns, begin, mid);
MergeSort(s, ns, mid + 1, end);
Merge(s, ns, begin, mid, end);
}
}
基数排序:(以十进制的整型为例)基数排序依次根据每个位上的数字,从低位到高位进行排序。这样,在低位排完序之后,高位一样的数字在同一桶内已经有序了。具体算法步骤如下:
- 分配,从个位开始,根据位上的数值,把元素分配到(0-9)的其中一个桶中。
- 收集,把上次排完序的元素,按桶的顺序再复制回数组中。
- 重复1,2步骤,直到分配到最高位。
基数排序,每次都是一个桶排序。基数排序的时间复杂度是O(dn),空间复杂度是O(mn).其中d是元素的最高位数,m是桶的数量,n是待排序列的元素数。
基数排序和桶排序都不是比较排序,其时间复杂度不受下限nlogn限制,当待排元素均匀的分布在每个区间时,比较适合使用桶排序。
其算法的代码实现如下:
int GetNumInPos(int num, int pos){
//在一个整型数中,取出第pos位的值,pos从0开始
int res = 0;
int key = 1;
for(int i = 0; i < pos; ++i){
key *= 10;
}
res = (num/key)%10;
return res;
}
int NumOfRules(int *s, int len){
//找到序列中的数最大的位数,如果不是纯数字的基数排序,则应该是排序的标准的个数
int Maxnum = 0;
for(int i = 0; i < len; i++){
int num = 0;
int tem = s[i];
while(tem != 0) {
num++;
tem = tem/10;
}
if(num > Maxnum) Maxnum = num;
}
return Maxnum;
}
void RadixSort(int *s, int len){
int temp[10][len];
memset(temp, 0, sizeof(temp));
//每一行的第一个用来记录该项元素的数量。
int Maxnum = NumOfRules(s, len);
for(int i = 0; i < Maxnum; ++i){
memset(temp, 0, sizeof(temp));
for(int j = 0; j < len; ++j){ //分配
int key = GetNumInPos(s[j], i);
int index = ++temp[key][0];
temp[key][index] = s[j];
}
int index = 0; //收集
for(int k = 0; k < 10; ++k){
for(int l = 1; l <= temp[k][0]; ++l){
s[index++] = temp[k][l];
}
}
}
}