七种常见经典排序算法分析与实现--C++
排序算法是非常常见也非常基础的算法,以至于大部分情况下它们都被集成到了语言的辅助库中。排序算法虽然已经可以很方便的使用,但是理解排序算法可以帮助我们找到解题的方向。
排序算法有:直接插入 希尔 冒泡 快速 选择 堆排序 归并
1. 插入排序—直接插入排序(Straight Insertion Sort)
基本思想:
将一个记录插入到已排序好的有序表中,从而得到一个新,记录数增1的有序表。即:先将序列的第1个记录看成是一个有序的子序列,然后从第2个记录逐个进行插入,直至整个序列有序为止。
要点:设立哨兵,作为临时存储和判断数组边界之用。
直接插入排序示例:
如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
算法实现:
void insert_sort()
{
int tmp; //哨兵
int i, j;
for (i = 1; i < num.size(); i++)
{
if (num[i] < num[i - 1])
{
tmp = num[i];
for (j = i - 1; j >= 0 && num[j] > tmp; j--)
{
num[j + 1] = num[j];
}
num[j + 1] = tmp;
}
}
cout << endl;
for (i = 0; i < num.size(); i++) cout << num[i] << " ";
}
效率:时间复杂度:O(n^2).
2. 插入排序—希尔排序(Shell`s Sort)
希尔排序是1959 年由D.L.Shell 提出来的,相对直接排序有较大的改进。希尔排序又叫缩小增量排序。希尔排序是非稳定排序算法。
基本思想:
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
操作方法:
排序过程:先取一个正整数d1
算法实现:
我们简单处理增量序列:增量序列d = {n/2 ,n/4, n/8 .....1} n为要排序数的个数
即:先将要排序的一组记录按某个增量d(n/2,n为要排序数的个数)分成若干组子序列,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。
void shell_sort()
{
int tmp;
int i,j;
int increment = num.size();
while (increment >= 1)
{
increment /= 2;
for (i = increment; i < num.size(); i++)
{
if (num[i] < num[i - increment])
{
tmp = num[i];
for (j = i - increment; j >= 0 && num[j] > tmp; j -= increment)
{
num[j + increment] = num[j];
}
num[j + increment] = tmp;
}
}
}
cout << endl;
for (i = 0; i < num.size(); i++) cout << num[i] << " ";
}
希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列d的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。目前还没有人给出选取最好的增量因子序列的方法。增量因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:增量因子中除1 外没有公因子,且最后一个增量因子必须为1。希尔排序方法是一个不稳定的排序方法。
效率:希尔排序时间复杂度的下界是n*log2n,因此中等大小规模表现良好。
3. 选择排序—简单选择排序(Simple Selection Sort)
基本思想:
在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
简单选择排序的示例:
算法实现:
第一趟,从n 个记录中找出关键码最小的记录与第一个记录交换;
第二趟,从第二个记录开始的n-1 个记录中再选出关键码最小的记录与第二个记录交换;
以此类推.....
第i 趟,则从第i 个记录开始的n-i+1 个记录中选出关键码最小的记录与第i 个记录交换,
直到整个序列按关键码有序。
void select_sort()
{
int tmp;
int i, j;
for (int i = 0; i < num.size(); i++)
{
tmp = i;
for (j = i + 1; j < num.size(); j++)
{
if (num[tmp] > num[j]) tmp = j;
}
if (tmp != i) swap(num[i], num[tmp]);
}
cout << endl;
for (i = 0; i < num.size(); i++) cout << num[i] << " ";
}
效率:时间复杂度为O(n^2),进行移动操作的时间复杂度为O(n)。简单选择排序是不稳定排序。
简单选择排序的改进——二元选择排序
简单选择排序,每趟循环只能确定一个元素排序后的定位。我们可以考虑改进为每趟循环确定两个元素(当前趟最大和最小记录)的位置,从而减少排序所需的循环次数。改进后对n个数据进行排序,最多只需进行[n/2]趟循环即可。
4. 选择排序—堆排序(Heap Sort)
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
基本思想
堆的定义如下:具有n个元素的序列(k1,k2,...,kn),当且仅当满足
时称之为堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最小项(小顶堆)。
若以一维数组存储一个堆,则堆对应一棵完全二叉树,且所有非叶结点的值均不大于(或不小于)其子女的值,根结点(堆顶元素)的值是最小(或最大)的。如:
(a)大顶堆序列:(96,83,27,38,11,09)
(b) 小顶堆序列:(12,36,24,85,47,30,53,91)
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。称这个过程为堆排序。
因此,实现堆排序需解决两个问题:
1. 如何将n 个待排序的数建成堆;
2. 输出堆顶元素后,怎样调整剩余n-1 个元素,使其成为一个新堆。
首先讨论第二个问题:输出堆顶元素后,对剩余n-1元素重新建成堆的调整过程。
调整小顶堆的方法:
1)设有m 个元素的堆,输出堆顶元素后,剩下m-1 个元素。将堆底元素送入堆顶((最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。
2)将根结点与左、右子树中较小元素的进行交换。
3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复方法 (2).
4)若与右子树交换,如果右子树堆被破坏,即右子树的根结点不满足堆的性质。则重复方法 (2).
5)继续对不满足堆性质的子树进行上述交换操作,直到叶子结点,堆被建成。
称这个自根结点到叶子结点的调整过程为筛选。如图:
再讨论对n 个元素初始建堆的过程。
建堆方法:对初始序列建堆的过程,就是一个反复进行筛选的过程。
1)n 个结点的完全二叉树,则最后一个结点是第个结点的子树。
2)筛选从第个结点为根的子树开始,该子树成为堆。
3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。
如图建堆初始过程:无序序列:(49,38,65,97,76,13,27,49)
算法的实现:
从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
void heap_adjust(int s,int len)
{
int i;
int tmp = num[s];
for (i = 2 * s + 1; i < len; i = 2 * i + 1)
{
if (num[i] < num[i + 1] && i < (len - 1)) i++;
if (tmp > num[i]) break;
num[s] = num[i];
s = i;
}
num[s] = tmp;
}
void heap_sort()
{
int i;
//创建一个大顶堆
for (i = num.size() / 2 - 1; i >= 0; i--)
{
heap_adjust(i,num.size());
}
//排序
cout << endl;
for (i = num.size() - 1; i >= 1; i--)
{
cout << num[0] << " ";
swap(num[0], num[i]);
heap_adjust(0, i);
}
cout << num[0] << endl;
}
分析:
设树深度为k,。从根到叶的筛选,元素比较次数至多2(k-1)次,交换记录至多k 次。所以,在建好堆后,排序过程中的筛选次数不超过下式:
而建堆时的比较次数不超过4n 次,因此堆排序最坏情况下,时间复杂度也为:O(nlogn )。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。
5. 交换排序—冒泡排序(Bubble Sort)
基本思想:
在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
冒泡排序的示例
算法的实现:
void bubble_sort()
{
bool flag = true;
int i, j;
for (i = 0; i < num.size(); i++)
{
flag = false;
for (j = num.size() - 1; j > i; j--)
{
if (num[j] < num[j - 1])
{
swap(num[j], num[j - 1]);
flag = true;
}
}
if (!flag) break;
}
cout << endl;
for (i = 0; i < num.size(); i++) cout << num[i] << " ";
}
6. 交换排序—快速排序(Quick Sort)
基本思想
一趟快速排序的算法是:
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
(a)一趟排序的过程:
算法的实现:
void quick_sort(int low,int high)
{
if (low >= high) return;
int pivotkey = num[low]; //存基准数
int i = low, j = high;
while (i != j)
{
//顺序很重要,从右向左开始找
while (num[j] >= pivotkey&&j > i) j--;
swap(num[i], num[j]);
//从左往右
while (num[i] <= pivotkey&&i < j) i++;
//交换两个数在数组中的位置
swap(num[i], num[j]);
}
quick_sort(low, i - 1); //处理左边
quick_sort(i + 1, high); //处理右边
}
分析:
快速排序是通常被认为在同数量级(O(nlogn))的排序方法中平均性能最好的。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。快速排序是一个不稳定的排序方法
7. 归并排序(Merge Sort)
基本思想:
归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序示例
合并方法:
设r[i…n]由两个有序子表r[i…m]和r[m+1…n]组成,两个子表长度分别为n-i +1、n-m。
算法的实现:
void merge(int start, int mid, int end)
{
int len1 = mid - start + 1;
int len2 = end - mid;
int len = end - start + 1;
int front[100];
int back[100];
int i, j, k;
for (i = 0; i < len1; i++)
front[i] = num[start + i];
for (j = 0; j < len2; j++)
back[j] = num[mid + j + 1];
for (i = 0, j = 0, k = start; i < len1&&j < len2&&k < end; k++)
{
if (front[i] < back[j])
{
num[k] = front[i];
i++;
}
else
{
num[k] = back[j];
j++;
}
}
while (i < len1) num[k++] = front[i++];
while (j < len2) num[k++] = back[j++];
}
void merge_sort(int start,int end)
{
if (start < end)
{
int mid = (start + end) / 2;
merge_sort(start, mid);
merge_sort(mid + 1, end);
merge(start, mid, end);
}
}
总结