注:本文参考了文章http://blog.csdn.net/hguisu/article/details/7776068,按照自己的理解另写了一篇。
概述
排序算法主要分为内排和外排,内部排序是指待排序记录放在计算机随机存储器中进行待排序过程;外部排序指待排序记录的数据量非常大,以致内存一次不能容纳全部记录,在排序过程中需要对外存访问的排序过程。此处仅讨论内部排序。
排序的主要算法如下图所示:
1. 插入排序
1.1 直接插入排序(Straight Insertion Sort)
基本思想:
把一个记录插入到一个已经排好序的有序表中,从而得到一个新的,记录数增1的有序表。
时间复杂度:O(n^2), 空间复杂度:O(1)。稳定排序算法。
下图示例了一个直接插入排序。
算法实现:
void insert_sort(int* arr, int len)
{
if(!arr || len < 2) return;
for(int i = 1; i < len; ++i)
{
if(arr[i-1] > arr[i]) //如果arr[i]最大,则直接插入arr[i]
{
int temp = arr[i];
int j = i-1;
while(j>= 0 && arr[j] > temp)
{
arr[j+1] = arr[j];
--j;
}
arr[j+1] = temp; //把元素插入到正确位置
}
}
return;
}
int main()
{
int arr[8] = {49, 38, 65, 97, 76, 13, 27, 49};
insert_sort(arr, 8);
for(auto item: arr)
std::cout<< item << " ";
std::cout << std::endl;
}
1.2 希尓排序(Shell's Sort)
void shell_insert_sort(int* arr, int len, int dk)
{
if(!arr || len < 2) return;
for(int i = dk; i < len; ++i)
{
if(arr[i-dk] > arr[i]) //如果arr[i]大,则查找元素i应该插入的正确位置
{
int temp = arr[i];
int j = i-dk;
while(j>= 0 && arr[j] > temp)
{
arr[j+dk] = arr[j];
j -= dk;
}
arr[j+dk] = temp; //把元素插入到正确位置
}
}
return;
}
void shell_sort(int* arr, int len)
{
if(!arr || len < 2) return;
int dk = len/2;
while(dk >= 1)
{
shell_insert_sort(arr, len, dk);
dk /= 2;
}
}
int main()
{
int arr[10] = {49, 38, 65, 97, 76, 13, 27, 49, 55, 4};
shell_sort(arr, 10);
for(auto item: arr)
std::cout<< item << " ";
std::cout << std::endl;
}
2. 快速排序
2.1 冒泡排序(Bubble Sort)
void bubble_sort(int* sour, int len)
{
if (!sour || len < 2) return;
for (int i = len; i > 0; --i)
for (int j = 0; j < i-1; ++j)
if (sour[j] > sour[j + 1])
{
sour[j] += sour[j + 1];
sour[j + 1] = sour[j] - sour[j + 1];
sour[j] = sour[j] - sour[j + 1];
}
return;
}
int main()
{
const int arrLen = 8;
int arr[arrLen] = { 49, 38, 65, 97, 76, 13, 27,49 };
bubble_sort(arr, arrLen);
for (int i = 0; i < arrLen; ++i)
std::cout << arr[i] << " ";
std::cout << std::endl;
system("pause");
}
对冒泡排序常见的改进方法是加入一标志性变量exchange,用于标志某一趟排序过程中是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。再分析下如下两种改进方法:
改进方法1:
设置一标志性变量pos,用于记录每趟排序中最后一次进行交换的位置。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。
改进算法:
void bubble_sort(int* arr, int len)
{
if(!arr || len < 2) return;
int i = len - 1;
while( i > 0)
{
int pos = 0;
for(int j = 0; j < i; ++j)
if(arr[j] > arr[j+1])
{
pos = j;
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
i = pos;
}
return;
}
int main()
{
int arr[8] = {49, 38, 65, 97, 76, 13, 27, 49};
bubble_sort(arr, 8);
for(auto item: arr)
std::cout<< item << " ";
std::cout << std::endl;
}
改进方法2:
传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值 , 我们考虑利用在每趟排序中进行正向和反向两遍冒泡的方法一次可以得到两个最终值 ( 最大者和最小者 ) , 从而使排序趟数几乎减少了一半。
改进算法:
void bubble_sort(int* arr, int len)
{
if(!arr || len < 2) return;
int low = 0;
int high = len - 1;
while( low < high )
{
for(int j = high; j > low; --j) //正向冒泡,找到最大的
if(arr[j] < arr[j-1])
{
int temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
--high; //修改high值,前移一位
for(int j = low; j < high; ++j) //反向冒泡,找到最小者
{
if(arr[j+1] < arr[j])
{
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
++low; //修改low值,后移一位
}
return;
}
2.2 快速排序(Quick Sort)
基本思想:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
时间复杂度:O(nloga(n)),最坏时间复杂度:O(n^2); 空间复杂度:O(1)。不稳定排序算法。
就平均时间而言,该方法是目前被认为最好的一种内排方法。但若初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。为改进之,通常以“三者取中法”来选取基准记录,即将排序区间的两个端点与中点三个记录关键码居中的调整为支点记录。
快速排序示例:
算法实现:
int partition(int* arr, int low, int high)
{
if(!arr) return low;
int pivotKey = arr[low];
while(low < high)
{
while(low < high && arr[high] >= pivotKey) --high;
arr[low] = arr[high];
while(low < high && arr[low] <= pivotKey) ++low;
arr[high] = arr[low];
}
arr[low] = pivotKey;
return low;
}
void quick_sort(int* arr, int low, int high)
{
if(!arr || low >= high) return;
int index = partition(arr,low, high);
quick_sort(arr, low, index -1);
quick_sort(arr, index+1, high);
return;
}
int main()
{
int arr[8] = {49, 38, 65, 97, 76, 13, 27, 49};
quick_sort(arr, 0, 7);
for(auto item: arr)
std::cout<< item << " ";
std::cout << std::endl;
}
3. 选择排序(Selection Sort)
3.1 简单选择排序
//找到最小元素的数组索引值
int selectMin(int* arr, int start, int len)
{
if (!arr || len < 0) return len;
int result = start;
for (int i = start + 1; i < len; ++i)
if (arr[i] < arr[result])
result = i;
return result;
}
void select_sort(int* arr, int len)
{
if (!arr || len < 2) return;
for (int i = 0; i < len-1; ++i)
{
// find the min element from the left array elements
int minIndex = selectMin(arr, i, len);
if (minIndex != len && minIndex != i)
{
// swap the element
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return;
}
int main()
{
const int arrLen = 8;
int arr[arrLen] = { 3,1,5,7,2,4,9,6 };
select_sort(arr, arrLen);
for (int i = 0; i < arrLen; ++i)
std::cout << arr[i] << " ";
std::cout << std::endl;
system("pause");
}
3.2 堆排序(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)
算法实现:
从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆的渗透函数,二是反复调用渗透函数实现排序的函数。
//从beg开始调整数组arr成为一个大顶堆
void adjustHeap(int* arr, int beg, int len)
{
if (!arr || len < 2) return;
int child = 2 * beg + 1;
while (child < len)
{
//chose the bigger element from left child and right child
if (child + 1 < len && arr[child] < arr[child + 1])
++child;
if (arr[child] > arr[beg])
{
int temp = arr[child];
arr[child] = arr[beg];
arr[beg] = temp;
beg = child;
child = 2 * beg + 1;
}
else
break;
}
return;
}
void buildHeap(int* arr, int len)
{
if (!arr || len < 2) return;
for (int i = (len - 1) / 2; i >= 0; --i)
adjustHeap(arr, i, len);
return;
}
4. 归并排序(Merging Sort)
基本思想:
归并排序(Merging Sort)法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
时间复杂度:O(nlog(n)),空间复杂度:O(n)。稳定排序算法。
归并排序示例:
2-路归并排序:
算法实现:
2-路归并排序的递归实现
//把sour[s... m)和s[m+1... n)合并为一个有序序列并放到dest[s...n)中
void merge(int* sour, int * dest, int s, int m, int n)
{
if (!sour || !dest) return;
int i = s;
int j = m;
int k = s;
while (i < m && j<n)
{
if (sour[i] < sour[j])
dest[k++] = sour[i++];
else
dest[k++] = sour[j++];
}
while (i < m)
dest[k++] = sour[i++];
while (j < n)
dest[k++] = sour[j++];
return;
}
//2-路归并排序递归实现
void MSort(int *sour, int *dest, int s, int t)
{
int* dest2 = sour;
if (s == t - 1) sour[s] = dest[t - 1];
else
{
int m = (s + t -1) / 2 +1; //平分sour数组
MSort(sour, dest2, s, m); //递归地b把sour[s…m]归并为有序的p2[s…m]
MSort(sour, dest2, m, t); //递归地把p[m+1…t)归并为有序的p2[m+1…t)
merge(dest2, dest, s, m + 1, t); //把sour[s…m]和sour[m+1…t)归并到dest[s…t)
}
}
void MergeSort_recursive(int *sour, int *dest, int n)
{ /*对顺序表*p 作归并排序*/
MSort(sour, dest, 0, n);
}
int main()
{
const int arrLen = 7;
int arr[arrLen] = { 49, 38, 65, 97, 76, 13, 27 };
int* b = new int[arrLen];
MergeSort_recursive(arr, b,arrLen);
for (int i = 0; i < arrLen; ++i)
std::cout << b[i] << " ";
std::cout << std::endl;
system("pause");
}
2-路归并排序的非递归实现:
//将r[s…m)和r[m …n)归并到辅助数组rf[s…n]
void merge(int* sour, int * dest, int s, int m, int n)
{
if (!sour || !dest) return;
int i = s;
int j = m;
int k = s;
while (i < m && j<n)
{
if (sour[i] < sour[j])
{
dest[k] = sour[i];
++i;
}
else
{
dest[k] = sour[j];
++j;
}
++k;
}
while (i < m)
{
dest[k] = sour[i];
++i;
++k;
}
while (j < n)
{
dest[k] = sour[j];
++j;
++k;
}
return;
}
//2-路归并排序非递归实现
void merge_sort(int* sour, int* dest, int len)
{
if (!sour || !dest || len < 2) return;
// num记录每次比较时子数组的长度
int num = 1;
while (num < len/2 + 2)
{
int i = 0;
while (i < len)
{
if (i + num * 2 < len)
merge(sour, dest, i, i + num, num * 2 + i); //merge同上。
else
merge(sour, dest, i, i + num, len);
i = i + num * 2;
}
num *= 2;
//这一步很重要,一定不要忘记
for (int i = 0; i < len; ++i)
sour[i] = dest[i];
}
}
int main()
{
const int arrLen = 7;
int arr[arrLen] = { 49, 38, 65, 97, 76, 13, 27 };
int* b = new int[arrLen];
merge_sort(arr, b, arrLen);
for (int i = 0; i < arrLen; ++i)
std::cout << b[i] << " ";
std::cout << std::endl;
system("pause");
}
总结
各种排序的稳定性,时间复杂度和空间复杂度总结:
对n较大的排序记录。一般的选择都是时间复杂度为O(nlog2n)的排序方法。
时间复杂度来说:
(1)平方阶(O(n2))排序
各类简单排序:直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlog2n))排序
快速排序、堆排序和归并排序;
(3)O(n1+§))排序,§是介于0和1之间的常数。
希尔排序
(4)线性阶(O(n))排序
基数排序,此外还有桶、箱排序。
说明:
当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);
而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);
原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。
稳定性:
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
选择排序算法准则:
每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。
选择排序算法的依据
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。
设待排序元素的个数为n.
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序 : 如果内存空间允许且要求稳定性的,
归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2) 当n较大,内存空间允许,且要求稳定性 =》归并排序
3)当n较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5)一般不使用或不直接使用传统的冒泡排序。
6)基数排序
它是一种稳定的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。