1.内排序和外排序
内排序:所有排序操作都在内存中完成
外排序:由于数据太大,因此将数据放在磁盘中,而排序通过磁盘和内存的数据阐传输才能进行
2.算法的时间复杂度和空间复杂度
时间复杂度:一个算法执行所耗费的时间
空间复杂度:执行完一个程序所需内存的大小
3.稳定排序和非稳定排序
简单地说就是所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,我们就说这种排序方法是稳定的。反之,就是非稳定的。
不稳定:选择排序、快速排序、堆排序、希尔排序、基数排序
稳定:插入排序、冒泡排序、归并排序、二叉树排序、计数排序、桶排序
常见的排序算法都是比较排序,非比较排序包括:计数排序、桶排序、基数排序
比较排序的时间复杂度通常为O(n2)或者O(nlogn),而非比较排序的时间复杂度可以达到O(n)
冒泡排序
冒泡排序(Bubble sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
##算法步骤
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
##实现代码
void bubble_sort(int num[], int len)
{
bool exchange;
for (int i = 0; i < len - 1; i++)
{
exchange = false;
for (int j = 0; j < len - i - 1; j++)
{
if (num[j] > num[j + 1])
{
int temp = num[j];
num[j] = num[j + 1];
num[j + 1] = temp;
exchange = true;
}
}
if (!exchange)
{
return;
}
}
}
选择排序
选择排序(Selection sort)也是一种简单直观的排序算法。
##算法步骤
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3)重复第二步,直到所有元素均排序完毕。
##实现代码
void selection_sort(int num[], int len)
{
for (int i = 0; i < len - 1; i ++)
{
int j = i;
for (int k = i + 1; k < len; k++)
{
if (num[k] < num[j])
{
j = k;
}
}
if (j != i)
{
int temp = num[j];
num[j] = num[i];
num[i] = temp;
}
}
}
插入排序
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
##算法步骤
1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
##实现代码
void insert_sort(int *num, int len)
{
for (int i = 1; i < len; i++)
{
int temp = num[i];
int j = i;
while (j > 0 && num[j - 1] > temp)
{
num[j] = num[j - 1];
j--;
}
num[j] = temp;
}
}
希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
##算法步骤
1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列个数k,对序列进行k 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
##实现代码1
void shell_sort(int num[], int len)
{
int i, j, k, group, temp;
for (group = len / 2; group > 0; group /= 2)
{
//对每个分组进行插入排序
for (i = 0; i < group; i++)
{
for (j = i + group; j < len; j += group)
{
if (num[j - group] > num[j])
{
temp = num[j];
k = j - group;
while(k >= 0 && num[k] > temp)
{
num[k + group] = num[k];
k -= group;
}
num[k + group] = temp;
}
}
}
}
}
##实现代码2
void shell_sort(int *num, int len)
{
for (int d = len / 2; d > 0; d /= 2)
{
//每个元素与其组内的元素进行插入排序
for (int i = d; i < len; i++)
{
int temp = num[i];
int j = i;
while (j >= d && num[j - d] > temp)
{
num[j] = num[j - d];
j -= d;
}
num[j] = temp;
}
}
}
两种实现方式的区别在于是否以组为单位进行处理,第一种实现方式以组为单位进行处理,处理完第1组再继续处理第二组,……,而第二种方式是将其中一组中的一个元素与其组内元素进行插入排序,然后对另一分组中的一个元素进行上述操作,……。
一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i += step_size而不是i++)。
例如,假设有这样一组数[ 13 14 94 33 82 25 59 94 65 23 45 27 73 25 39 10 ],如果我们以步长为5开始进行排序,我们可以通过将这列表放在有5列的表中来更好地描述算法,这样他们就应该看起来是这样:
13 14 94 33 82
25 59 94 65 23
45 27 73 25 39
10
然后对每列进行排序:
10 14 73 25 23
13 27 94 33 39
25 59 94 65 82
45
将上述四行数字,依序接在一起时我们得到:[ 10 14 73 25 23 13 27 94 33 39 25 59 94 65 82 45 ].这时10已经移至正确位置了,然后再以3为步长进行排序:
10 14 73
25 23 13
27 94 33
39 25 59
94 65 82
45
排序之后变为:
10 14 13
25 23 33
27 25 59
39 65 73
45 94 82
94
最后以1步长进行排序(此时就是简单的插入排序了)。
归并排序
算法示意图:
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
##算法步骤
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
设定两个指针,最初位置分别为两个已经排序序列的起始位置
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针达到序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
##实现代码
//first和end为num第一个元素和最后一个元素的索引
void merge_sort(int num[], int first, int end)
{
if (first < end)
{
int mid = (first + end) / 2;
merge_sort(num, first, mid);
merge_sort(num, mid + 1, end);
merge(num, first, mid, end);
}
}
//合并序列[start, mid]、[mid+1, end]
void merge(int num[], int first, int mid, int end)
{
int n1 = mid - first + 1;
int n2 = end - mid;
int* L = new int[n1];
int* R = new int[n2];
for(int i = 0; i < n1; i++)
{
L[i] = num[first + i];
}
for (int j = 0; j < n2; j++)
{
R[j] = num[mid + j + 1];
}
int i = 0;
int j = 0;
int k = first;
while(i < n1 && j < n2)
{
if (L[i] < R[j])
{
num[k++] = L[i++];
}
else
{
num[k++] = R[j++];
}
}
while (i < n1)
{
num[k++] = L[i++];
}
while (j < n2)
{
num[k++] = R[j++];
}
delete [] L;
delete [] R;
}
另一种实现方式:
void merge(int nums[], int left, int mid, int right) {
int *temp = new int[right - left + 1];
int i = left;
int j = mid + 1;
int x = 0;
while (i <= mid && j <= right) {
if (nums[i] < nums[j]) {
temp[x++] = nums[i++];
} else {
temp[x++] = nums[j++];
}
}
while (i <= mid) {
temp[x++] = nums[i++];
}
while (j <= right) {
temp[x++] = nums[j++];
}
for (int k = 0; k < right - left + 1; k++) {
nums[left + k] = temp[k];
}
delete [] temp;
}
void merge_sort(int nums[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
merge_sort(nums, 0, mid);
merge_sort(nums, mid + 1, right);
merge(nums, 0, mid, right);
}
}
快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
##算法步骤
1 从数列中挑出一个元素,称为 “基准”(pivot),
2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
下面通过一个例子介绍快速排序算法的思想,假设要对数组a[10]={6,1,2,7,9,3,4,5,10,8}进行排序,首先要在数组中选择一个数作为基准值,这个数可以随意选择,在这里,我们选择数组的第一个元素a[0]=6作为基准值,接下来,我们需要把数组中小于6的数放在左边,大于6的数放在右边,怎么实现呢?
我们设置两个“哨兵”,记为“哨兵i”和“哨兵j”,他们分别指向数组的第一个元素和最后一个元素,即i=0,j=9。首先哨兵j开始出动,哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。
最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。此时就需要交换i和j指向的元素的值。
交换之后的数组变为a[10]={6,1,2,5,9,3,4,7,10,8}:
第一次交换至此结束。接下来,由于哨兵i和哨兵j还没有相遇,于是哨兵j继续向前,发现比6小的4之后停下;哨兵i继续向前,发现比6大的9之后停下,两者再进行交换。交换之后的数组变为a[10]={6,1,2,5,4,3,9,7,10,8}。
第二次交换至此结束。接下来,哨兵j继续向前,发小比6小的3停下来;哨兵i继续向前,发现i==j了!!!于是,这一轮的探测就要结束了,此时交换a[i]与基准的值,数组a就以6为分界线,分成了小于6和大于6的左右两部分:a[10]={3,1,2,5,4,6,9,7,10,8}。
至此,第一轮快速排序完全结束,接下来,对于6左边的半部分3,1,2,5,4,执行以上过程;对于6右边的半部分9,7,10,8,执行以上过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列:1 2 3 4 5 6 7 8 9 10,到此,排序完全结束。
##实现代码
#include <stdio.h>
void QuickSort(int array[], int low, int high)
{
int i = low;
int j = high;
if(i > j)
return;
int temp = array[low];
while(i != j)
{
while(array[j] >= temp && i < j)
j--;
while(array[i] <= temp && i < j)
i++;
if(i < j)
swap(array[i], array[j]);
}
//将基准temp放于自己的位置,(第i个位置)
swap(array[low], array[i]);
QuickSort(array, low, i - 1);
QuickSort(array, i + 1, high);
}
int main()
{
int a[8] = {1,4,7,2,3,6,5,8};
QuickSort(a, 0, 7);
for(int i = 0; i < 8; i++)
{
printf("%d\n",a[i]);
}
return 0;
}
堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的平均时间复杂度为Ο(nlogn) 。
##算法步骤
1)创建一个堆H[0…n-1]
2)把堆首(最大值)和堆尾互换
3)把堆的尺寸缩小1,并调用shift_down(0),目的是把新的数组顶端数据调整到相应位置
4) 重复步骤2,直到堆的尺寸为1
##实现代码
void quick_sort(int num[], int left, int right)
{
if (left < right)
{
int i = left;
int j = right;
int x = num[i];
while (i < j)
{
while (i < j && num[j] >= x)
{
j--;
}
if(i < j)
{
num[i++] = num[j];
}
while (i < j && num[i] < x)
{
i++;
}
if(i < j)
{
num[j--] = num[i];
}
}
num[i] = x;
quick_sort(num, left, i - 1);
quick_sort(num, i + 1, right);
}
}
计数排序
个人认为计数排序局限性比较大,但还是把它一起总结一下。
##算法思想
假定输入是有一个小范围内的整数构成的(比如年龄等),利用额外的数组去记录元素应该排列的位置,思想比较简单。
特点:在一定限制下时间复杂度为O(n),额外空间O(n)(需要两个数组),稳定排序!
##实现代码
void quick_sort(int num[], int left, int right)
{
if (left < right)
{
int i = left;
int j = right;
int x = num[i];
while (i < j)
{
while (i < j && num[j] >= x)
{
j--;
}
if(i < j)
{
num[i++] = num[j];
}
while (i < j && num[i] < x)
{
i++;
}
if(i < j)
{
num[j--] = num[i];
}
}
num[i] = x;
quick_sort(num, left, i - 1);
quick_sort(num, i + 1, right);
}
}
桶排序
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
说基数排序之前,我们简单介绍桶排序:
算法思想:是将阵列分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递回方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的阵列内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到 O(n log n) 下限的影响。
简单来说,就是把数据分组,放在一个个的桶中,然后对每个桶里面的在进行排序。
例如要对大小为[1…1000]范围内的n个整数A[1…n]排序
首先,可以把桶设为大小为10的范围,具体而言,设集合B[1]存储[1…10]的整数,集合B[2]存储 (10…20]的整数,……集合B[i]存储( (i-1)10, i10]的整数,i = 1,2,…100。总共有 100个桶。
然后,对A[1…n]从头到尾扫描一遍,把每个A[i]放入对应的桶B[j]中。再对这100个桶中每个桶里的数字排序,这时可用冒泡,选择,乃至快排,一般来说任何排序法都可以。
最后,依次输出每个桶里面的数字,且每个桶中的数字从小到大输出,这样就得到所有数字排好序的一个序列了。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是
O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)
从上式看出,当m接近n的时候,桶排序复杂度接近O(n)
当然,以上复杂度的计算是基于输入的n个数字是平均分布这个假设的。这个假设是很强的 ,实际应用中效果并没有这么好。如果所有的数字都落在同一个桶中,那就退化成一般的排序了。
前面说的几大排序算法 ,大部分时间复杂度都是O(n2),也有部分排序算法时间复杂度是O(nlogn)。而桶式排序却能实现O(n)的时间复杂度。但桶排序的缺点是:
1)首先是空间复杂度比较高,需要的额外开销大。排序有两个数组的空间开销,一个存放待排序数组,一个就是所谓的桶,比如待排序值是从0到m-1,那就需要m个桶,这个桶数组就要至少m个空间。
2)其次待排序的元素都要在一定的范围内等等。
##实现代码
void quick_sort(int num[], int left, int right)
{
if (left < right)
{
int i = left;
int j = right;
int x = num[i];
while (i < j)
{
while (i < j && num[j] >= x)
{
j--;
}
if(i < j)
{
num[i++] = num[j];
}
while (i < j && num[i] < x)
{
i++;
}
if(i < j)
{
num[j--] = num[i];
}
}
num[i] = x;
quick_sort(num, left, i - 1);
quick_sort(num, i + 1, right);
}
}
参考文章:https://blog.csdn.net/u011331383/article/details/43798223