目录
排序的基本概念
给定多条记录的一个序列 ,其对应的关键字为
,
经过排序可以得到的输出为:新序列 及关键字
对排序算法的评价标准:一般以关键字的比较次数和数据的移动次数来度量排序算法的时间复杂度。
排序算法的效率往往与数据的初始顺序有很大的关系,所以在分析排序算法的性能时,要考虑最好情况、最坏情况和平均情况下的时间复杂度和空间复杂度。
1. 插入排序
直接插入排序
直接插入排序:由n-1趟排序组成。第p趟排序后保证从第0个位置到第p个位置上的元素为有序状态。第p+1趟排序是将p+2个元素插入到前面p+1个元素的有序表中。
时间复杂度:最好O(n) , 平均O(n^2) ,最坏O(n^2);
空间复杂度: O(1);
是稳定的。
template<class T>
void InsertionSort(T data[], int n)
{
for (int i = 1; i < n; i++)
{
T temp = data[i];
int j = i - 1;
for (; j >= 0; j--)
{
if (temp < data[j])
data[j + 1] = data[j];
else
break;
}
data[j + 1] = temp;
}
}
- 折半插入排序
时间复杂度:最好 O(n lgn) ,平均 O(n^2) ,最坏 O(n^2)。 最坏情况下比直接插入好
空间复杂度:O(1)
是稳定的。
template<class T>
void BinaryInsertionSort(T data[], int n)
{
for (int i = 1; i < n; i++)
{
T temp = data[i]; //保存待插入数据
int left = 0, right = i-1;int mid;
while (left<=right)
{
mid = (left + right) / 2;
if (data[mid] < temp)
left = mid + 1;
else
right = mid - 1;
}
for (int j = i - 1; j >=left ; j--)
data[j + 1] = data[j];
data[left] = temp;
}
}
希尔排序
希尔排序的基本思想:先将待排序数据序列划分成为若干子序列分别进行直接插入排序,待整个序列中的数据基本有序后,再对全部数据进行一次直接插入排序。
时间复杂度在O(n lgn)和O(n^2)之间,大致为O(n^1.3).
不是稳定的。
template<class T>
void ShellSort(T data[], int n)
{
int d = n / 2;
while (d >= 1)
{
for(int k = 0; k < d; k++)
{
for(int i = k + d; i < n; i += d)//对每个子序列执行直接插入排序
{
T temp = data[i];
int j = i - d;
while (j >= k && data[j] > temp)
{
data[j + d] = data[j];
j -= d;
}
data[j + d] = temp;
}
}
d = d / 2;
}
}
2. 交换排序
冒泡排序
冒泡排序:通过不断的比较相邻元素的大小,然后决定是否对两个元素进行交换操作,从而达到排序的目的。
改进:当不发生交换时,说明序列已经有序,可以提前结束算法的执行。
时间复杂度:最好O(n) ,平均O(n^2),最差O(n^2)
无需额外空间。
是一个稳定的排序算法。
template<class T>
void BubbleSort(T data[], int n)
{
int flag =0;
for (int i = 0; i < n; i++)
{
flag =0;
for (int j = 0; j < n - i-1; j++)
{
if (data[j] > data[j + 1])
{
flag = 1;
T temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
}
}
if(flag==0) return ;
}
}
快速排序
快速排序算法主要由三步组成:
- 分割:取序列的一个元素作为轴元素,利用这个轴元素把序列分为三段,使得所有小于等于轴元素的放在轴元素的左边,大于轴元素的放到轴元素的右边。
- 分治:对左段和右段中的元素递归调用第1步,分别对左段和右段中的元素进行排序。
- 合并:不需要执行其他操作。
快排的最坏情况出现在输入序列有序的时候,每一次分割都将轴元素划分在序列的一端。因此在整个过程中的比较次数为n(n-1)/2,时间复杂度为O(n^2)。
最好情况:每次分割都是最平衡的,每个子序列的长度为n/2。时间复杂度为O(nlgn);
平均时间复杂度:O(nlgn)
是一种不稳定的算法。
//partition 实现对data的分割,并返回划分后轴元素对应的位置
template<class T>
int Partition1(T data[], int left, int right)//第一种分割方法:
{
T pivot = data[left];
while (left < right)
{
while (left<right && data[right]>pivot)
right--;
data[left] = data[right];
while (left < right && data[left] <= pivot)
left++;
data[right] = data[left];
}
data[left] = pivot;
return left;
}
template<class T>
int Partition2(T data[], int start, int end)//第二种分割方法:
{
T pivot = data[start];
int left = start, right = end;
while (left <= right)
{
while (left <= right && data[left] <= pivot)
left++;
while (left <= right && data[right] > pivot)
right--;
if (left < right)
{
std::swap(data[left],data[right]);
left++;
right--;
}
std::swap(data[start], data[right]);
return right;
}
}
template<class T>
void QuickSort(T data[], int left, int right)
{
if (left < right)
{
int p = Partition1(data, left, right);
QuickSort(data, left, p - 1);
QuickSort(data, p + 1, right);
}
}
3. 选择排序
简单选择排序
简单选择排序:利用线性查找的方法从一个序列中找到最小的元素,即第i趟的排序操作为:通过n-i次关键字的比较,从n-i+1个元素中选出关键字最小的元素并和第i-1个元素交换。
时间复杂度:最好、最坏、平均 O(n^2)
空间复杂度:O(1)
是不稳定的算法。
template<class T>
void SelectionSort(T data[], int n)
{
for (int i = 1; i < n; i++)
{
int k = i - 1;
for (int j = i; j < n; j++)
{
if (data[k] > data[j])
k = j;
}
if (k != i - 1)
{
T t = data[k];
data[k] = data[i - 1];
data[i - 1] = t;
}
}
}
堆排序
简单选择排序:利用线性查找的方法从一个序列中找到最小的元素。
堆排序:利用最大堆结构找到序列中的最大值,从而实现选择排序。具体步骤:
- 将初始待排序的数据初始化为最大堆(最大堆:在最大堆中要求每个结点对应的值都应该大于或等于该结点的子节点的值)
- 将堆顶元素和当前最后一个元素进行交换,n=n-1
- 调整堆结构
- 如果n>1,则重复第2步和第3步。
template<class T>
void SiftDown(T data[], int i, int n)
{
int l = 2 * i + 1, r = 2 * i + 2, max = i;
if (l < n && data[max] < data[l])
max = l;
if (r < n && data[max] < data[r])
max = r;
if (max != i)
{
T t = data[max];
data[max] = data[i];
data[i] = t;
SiftDown(data, max, n);
}
}
template<class T>
void BuildHeap(T data[], int n)
{
int p = n / 2-1;
for (int i = p; i >= 0; i--)
SiftDown(data, i, n);
}
template<class T>
void HeapSort(T data[],int n)
{
BuildHeap(data, n);
printA(data, n);
for (int i = n - 1; i > 0; i--)
{
std::swap(data[0], data[i]);
SiftDown(data, 0, i);
printA(data, n);
}
}
4. 归并排序
归并排序的思想:若一个序列只有一个元素,则它是有序的,归并排序不执行任何操作。否则,归并排序算法的递归步骤为:
- 先把序列划分为长度基本相等的子序列。
- 对每个子序列归并排序。
- 把排好序的子序列合并为最后的结果。
#include<iostream>
template<class T>
void Merge(T data[], int start, int mid, int end)
{
int len1 = mid - start + 1, len2 = end - mid;
T * left = new T[len1];
T * right = new T[len2];
for (int i = 0; i < len1; i++)
left[i] = data[i + start];
for (int j = 0; j < len2; j++)
right[j] = data[j + mid + 1];
int i = 0, j = 0, k ;
for (k=start;k < end;k++)
{
if (i == len1 || j == len2)
break;
if (left[i] <= right[j])
data[k] = left[i++];
else
data[k] = right[j++];
}
while (i < len1)
data[k++] = left[i++];
while (j < len2)
data[k++] = right[j++];
delete[]left;
delete[]right;
}
template<class T>
void MergeSort(T data[], int start,int end)
{
if (start < end)
{
int mid = (start + end) / 2;
MergeSort(data, start, mid);
MergeSort(data, mid+1, end);
Merge(data, start, mid, end);
}
}
总结
基于比较的排序算法的时间复杂度的下界为O(nlgn)
排序算法 | 时间复杂度 | 空间复杂度 | 是否稳定 | ||
最好 | 最坏 | 平均 | |||
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(1) | 不稳定 | ||
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlgn) | O(n^2) | O(nlgn) | O(1) | 不稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlgn) | O(nlgn) | O(nlgn) | O(1) | 不稳定 |
归并排序 | O(nlgn) | O(nlgn) | O(nlgn) | O(n) | 稳定 |
5. 基数排序
基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。
多关键字排序的一个典型问题就是对扑克牌进行排序。花色的优先级最高,为主关键字,面值为次关键字。
1. 高位优先法:先按照优先级最高的关键字进行排序,在按照次优先级关键字排序。
2. 低位优先级:先按照优先级最低的关键字进行排序,在按照优先级较高一层的关键字排序。
对于关键字是较大的十进制数值,可以用低位优先的基数排序算法排序。
算法复杂度O(dn),是一种稳定的排序算法。
6. 外部排序
内部排序算法:待排序的数据全部存储在内存中,前面介绍的都是内部排序算法。但是当数据规模很大时,内存是远远不够的,这是就需要利用外部算法来排序。
外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次读入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序的目的。
外部排序分为两个阶段:预处理和归并处理。在预处理阶段,根据内存大小将有n个记录的磁盘文件分批读入内存,采取有效的内存排序方法进行排序,将其预处理为若干有序的文件,这些有序子文件成为“初始顺串”。在归并阶段,利用归并的方法将这些初始顺串逐趟合并成一个有序的文件。
置换选择排序-阶段一:预处理阶段
一、初始化堆:
- 从磁盘读入M个记录放入到内存数组中。
- 设置堆末尾标准LAST=M-1;
- 建立最小堆。
二、重复一下步骤直到堆为空,即LAST=-1:
- 把具有最小关键字的记录Min也就是根节点送到输出缓存区。
- 设R是输入缓冲区中的下一条记录,如果R的关键字大于刚刚输出的关键字Min,则把R放到根节点;否则使用数组中LAST位置的记录代替根节点,将R放入到LAST所在位置,并设置LAST=LAST-1;
- 重新调整堆。
三、生成一个顺串后,要重新将数据调整为最小堆,并设置LAST=M-1;重复过程 二)生成下一个顺串。
预计平均情况下初始顺串长度是数组长度的两倍,即2M。
多路归并-阶段二:归并
赢者树

赢者树的叶子结点是初始顺串中当前要处理的数据,内部结点是其左右子节点的最小值所在的顺串的索引(假设从小到大排序)。
树的根节点就是最终的赢者的索引,即下一个要输出的记录对应的顺串索引。
输出该记录后,该顺串的下一个记录成为该顺串的当前记录,相应的关键字进入赢者树,此时需要调整赢者树。调整过程为
- 将发生改变的叶节点作为当前节点p;
- 如果p是根节点则调整结束。否则,比较p与其兄弟节点(如果有的话)的关键字,用较小的关键字修改p的父节点parent的节点值。
- p=parent,重复过程2.