排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
一、冒泡排序
基本思想:依次比较相邻的两个数,将小数放在前面,大数放在后面。即首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复以上过程,仍从第一对数开始比较,将小数放前,大数放后,一直比较到倒数第二个数,将小数放前,大数放后,第二趟结束,在倒数第二个数中得到一个新的最大数。如此下去,直至最终完成排序。由于在排序过程中总是小数往前放,大数往后放,每次循环中一个最大的数沉底成为最后一个元素,一些较小的数如同气泡一样上浮一个位置,所以称作冒泡排序。
冒泡排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n²)。通过添加标志位可将最小时间复杂度降为O(n)。
void BubbleSort(vector& a) {
for (int i = 0; i < a.size() - 1; ++i) {
for (int j = 0; j < a.size() - i - 1; ++j) {
if (a[j] > a[j + 1])
swap(a[j], a[j + 1]);
}
}
}
void BubbleSort(vector& a) {
bool didSwap;
for (int i = 0; i < a.size() - 1; ++i) {
didSwap = false;
for (int j = 0; j < a.size() - i - 1; ++j) {
if (a[j] > a[j + 1]) {
swap(a[j], a[j + 1]);
didSwap = true;
}
}
if (!didSwap)
break;
}
}
二、直接插入排序
基本思想:将一个数插入到已排序好的有序数组中,从而得到一个新的元素数量增1的有序数组。即:先将数组的第1个元素看成是一个有序的子数组,然后从第2个数逐个进行插入,直至整个数组有序为止。
最差情况下,直接插入排序的最大时间复杂度为O(n²),最小时间复杂度为O(n),平均时间复杂度为O(n²)。
void InsertSort(vector& a) {
for (int i = 1; i < a.size(); ++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; //插入到正确位置
}
}
基本思想:在要排序的一组数中,选出最小的一个数与第1个位置的数交换;然后在剩下的数当中再找最小的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。
简单选择排序最大的特点是交换移动数据的次数相当少,这样也节约了时间。简单选择排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n²)。尽管复杂度与冒泡排序相同,但简单选择排序的性能还是要略优于冒泡排序。
void SelectSort(vector& a) {
for (int i = 0; i < a.size() - 1; ++i) {
int min = i; //将当前下标定义为最小值下标
for (int j = i + 1; j < a.size(); ++j) { //找出剩下的数中的最小值下标
if (a[j] < a[min])
min = j;
}
if (min != i)
swap(a[i], a[min]);
}
}
四、快速排序
基本思想:
4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。
快速排序的最大时间复杂度为O(n²),最小时间复杂度为O(n*logn),平均时间复杂度为O(n*logn)。注意:快速排序是一种不稳定的排序方式,其性能依赖于原始数组的有序程度,更进一步分析,就是依赖于轴值元素的选择。快速排序是通常被认为在同数量级(O(nlog2n))的排序方法中平均性能最好的。但若初始序列有序或基本有序时,快速排序反而退化为冒泡排序。
int partition(vector& a, int low, int high) {
int pivotkey = a[low];
while (low < high) {
while (low < high && a[high] >= pivotkey)
--high;
swap(a[low], a[high]);
while (low < high && a[low] <= pivotkey)
++low;
swap(a[low], a[high]);
}
return low;
}
void QuickSort(vector& a, int low, int high) {
if (low < high) {
int pivot = partition(a, low, high);
QuickSort(a, low, pivot - 1);
QuickSort(a, pivot + 1, high);
}
}
五、归并排序
归并排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n*logn)。归并排序不依赖于原始数组的有序程度。
void Merge(vector& a, int low, int mid, int high, vector& temp) {
int i = low, j = mid + 1, k = 0;
while (i <= mid && j <= high) {
if (a[i] <= a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++];
}
while (i <= mid)
temp[k++] = a[i++];
while (j <= high)
temp[k++] = a[j++];
for (i = 0; i < k; ++i)
a[low + i] = temp[i];
}
void MergeSort(vector& a, int low, int high, vector& temp) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(a, low, mid, temp); //左边有序
MergeSort(a, mid + 1, high, temp); //右边有序
Merge(a, low, mid, high, temp); //将两个有序序列合并
}
}
六、堆排序
堆排序是一种树形选择排序,是对直接选择排序的有效改进。堆排序的最大时间复杂度,最小时间复杂度和平均时间复杂度均为O(n*logn)。堆排序和归并排序一样,不依赖于原始数组的有序程度。
基本思想:
堆的定义如下:具有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 HeapAdjust(vector& a, int parent, int length) {
int tmp = a[parent];
int child = 2 * parent + 1; //左孩子节点的位置
while (child < length) {
if (child + 1 < length && a[child] < a[child + 1])
++child;//找到比当前待调整节点大的孩子节点
if ( a[parent] < a[child]){// 如果父结点小于较大的子结点
a[parent] = a[child]; // 那么把较大的子结点往上移动,替换它的父结点
parent = child; // 重新设置s ,即待调整的下一个结点的位置
child = 2 * parent + 1;
}
else
break; // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
a[parent] = tmp;// 当前待调整的结点放到比其大的孩子结点位置上
}
}
void HeapBuild(vector& a) {
//最后一个有孩子的节点的位置为(a.size()-1)/2
for (int i = (a.size() - 1) / 2; i >= 0; --i)
HeapAdjust(a, i, a.size());
}
void HeapSort(vector& a) {
HeapBuild(a); //构建初始堆
for (int i = a.size()-1; i >=0 ; --i) {//从最后一个元素开始对序列进行调整
swap(a[0], a[i]);
HeapAdjust(a, 0, i);//每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
}
}
总结
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
选择排序算法准则: 每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。 选择排序算法的依据 影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n. 1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短; 堆排序 : 堆排序适合于数据量非常大的场合(百万数据)。 堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。 堆排序会将所有的数据建成一个堆,最大的数据在堆顶,然后将堆顶数据和序列的最后一个数据交换。接下来再次重建堆,交换数据,依次下去,就可以排序所有的数据。 归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。 2)当n较大,内存空间允许,且要求稳定性 =》归并排序 3)当n较小,可采用直接插入或直接选择排序。 直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。 直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序 4)一般不使用或不直接使用传统的冒泡排序。
文章部分内容摘自http://blog.csdn.net/hguisu/article/details/7776068