1.直接插入排序(Straight Insertion Sort)
直接插入排序(Straight Insertion Sort)是一种最简单的排序方法。基本操作是将当前元素插入到已排好序的有序表中,最终得到完全有序的元素集合。
例如,对元素集合{49,38,65,97,76,13,27}进行排序的操作如下:
i = 1:{[38],49,65,97,76,13,27}
i = 2:{[38,49],65,97,76,13,27}
i = 3:{[38,49,65],97,76,13,27}
i = 4:{[38,49,65,97],76,13,27}
i = 5:{[38,49,65,76,97],13,27}
i = 6:{[13,38,49,65,76,97],27}
i = 7:{[13,27,38,49,65,76,97]}
经过6趟排序操作后可得到完全有序的元素集合。C++实现代码如下:
void InsertSort(int a[], int n)
{
int i, j, tmp;
for(i = 1; i < n; i++)
{
tmp = a[i];
for(j = i; j > 0 && tmp < a[j - 1]; j--)
{
a[j] = a[j - 1];
}
a[j] = tmp;
}
}
直接插入排序只需要一个记录的辅助空间,但是其时间复杂度却不理想。在排序过程中,当待排序的关键字非递减有序时,所需进行的关键字比较次数为n-1,记录不需要移动。而当待排序的关键字非递增有序时,所需要进行的关键字的比较字数为(n+2)(n-1)/2,记录移动次数为(n+4)(n-1)/2,此时关键字比较和记录移动的总次数约为n*n/4。
因此直接插入排序的时间为O(n*n)。
2.折半插入排序(Binary Insertion Sort)
折半插入排序是直接插入排序的优化,可以减少关键字的比较次数。具体实现如下:
void BinaryInsertionSort(int *array, int length)
{
//从数组第二个元素开始依次将其插入到数组中,使得数组有序
for(int i = 1; i < length; i++)
{
int low = 0, high = i - 1, j = 0;
while(low <= high)
{
j = (low + high) / 2;
if(array[j] <= array[i])
{
low = j + 1;
}
else
{
high = j - 1;
}
}
//如果在已排好序的序列中加入array[i]后序列无序
//则调整array[i]的位置使得序列有序
if(j < i)
{
//保存array[i]
int tmp = array[i];
for(int k = i; k > j; k--)
{
array[k] = array[k - 1];
}
//将array[i]放到正确位置
array[k] = tmp;
}
}
}
3.起泡排序(Bubble Sort)
起泡排序的过程比较简单,将第一个元素与第二个元素进行比较,若为逆序则交换两个记录,再将第二个元素与第三个元素比较,依次类推,直到第n-1个元素与第n个元素比较,最终得到有序的元素集合。起泡排序每一趟都将最大的元素放到正确的位置上。判断起泡排序结束的标志是在一趟排序过程中不存在元素交换。c++实现代码如下:
void BubbleSort(int a[], int n)
{
int i, j, tmp, flag;
for(i = 0; i < n - 1; i++)
{
flag = 0; //标志位,如果在一趟排序过程中不存在元素交换,排序结束
for(j = 0; j < n - i; j++)
{
if(a[j] > a[j + 1])
{
tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
flag = 1;
}
}
if(flag == 0)
break;
}
}
起泡排序总的时间复杂度为O(n*n)。
4.快速排序(Quick Sort)
快速排序是对起泡排序的改进。基本思路是将数组A[p..r]划分为两个也数组A[p..q-1]和A[q+1..r],使得A[p..q-1]中的每个元素都小于等于A[q],而且小于等于A[q+1..r]中的元素。再通过递归调用快速排序,对子数组A[p..q-1]及A[q+1..r]进行排序。通过排序后得到有序的数组A[p..r]
快速排序的关键在于如何对数组进行划分,一般取x=A[r]作为主元(pivot element),以它为界限来对数组进行划分。使得划分的结果是在x左侧的元素都小于等于x,且小于等于x右侧的元素。
具体实现如下:
//快速排序序列划分
//划分结果是使得数组中处于某一个元素左边的所有元素都不大于该元素
//而处于该元素右边的元素都不小于该元素
int Partition(int *array, int p, int r)
{
int x = array[r]; //取数组元素array[r]作为锚点
int i = p - 1;
int tmp = 0;
//对数组元素array[p]到array[r-1]进行分析
//如果array[j]不大于array[r]则将i加1然后交换array[i]和array[j]的值
for(int j = p;j < r; j++)
{
if(array[j] <= x)
{
i++;
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
//交换array[i+1]和array[r]的值
//使得array[p..i]不大于array[i],array[i+1..r]不小于array[i]
tmp = array[i + 1];
array[i + 1] = array[r];
array[r] = tmp;
//返回数组划分点的索引
return i + 1;
}
//调用Partition对数组进行划分
//再对划分的两部分递归调用QuickSort
void QuickSort(int *array, int p, int r)
{
//递归终止条件为p>=r
if(p < r)
{
int q = Partition(array, p, r);
QuickSort(array, p, q - 1);
QuickSort(array, q + 1, r);
}
}
快速排序最坏情况下时间复杂度为O(n*n),此时快速排序退化为一般的比较排序。但是其平均性能较好,期望时间复杂度为O(nlgn)。而且快速排序是就地排序,因此应用较为广泛。
5.计数排序
计数排序是假设数组a[n]的元素都介于0到k之间,小于等于a[i]的元素个数为j,则在完成排序时a[j]在结果集合中的索引即为j(索引从0开始)。
#define MAX 100 //待排序的元素最大值
void CountingSort(long *a, long n)
{
int i, j;
int c[MAX] = {0}; //计数数组
for(i = 0; i < n; i++)
{
//数组c的下标是数组a的元素
c[a[i]]++;
}
j = 0;
for(i = 0; i < MAX; i++)
{
while(c[i]-- > 0)
{
a[j++] = i;
}
}
}
计数排序时间复杂度为O(MAX+n),其中MAX为待排序的元素最大值,n为待排序元素的个数。计数排序的下界优于快速排序,但是计数排序也有局限性,当MAX远大于n时,不宜采用计数排序,而当MAX和n较接近时,优先考虑计数排序,此时的时间复杂度为O(n)。
6.基数排序
基数排序是基于待排序元素每一位关键字进行的。首先依据待排序元素个位数字进行排序,再依据十位数字排序,依次类推直至达到待排序元素最高位。
基数排序分两种情况,第一种情况是待排序所有元素的位数都相同。
#define NUM 4 //待排序元素的位数
void RadixSort(int *arr, int len)
{
int i, j, div, c[10];
int *b = new int[len]; //过渡数组,保存每趟排序中间结果
int *key = new int[len]; //存储数组元素某一位的数字
//对包含len个元素且每个元素是NUM位的数组循环NUM次
//每次循环利用计数排序对数组元素进行排序
for(j = 1; j <= NUM; j++)
{
div = 1;
for(i = 1; i < j; i++)
{
div *= 10;
}
//取得数组元素第j位数字并存入数组key中
for(i = 0; i < len; i++)
{
key[i] = arr[i] / div % 10;
}
//数组c用来保存第j次循环数组元素第j位数字出现的次数
memset(c, 0, sizeof(c));
//数字key[i]出现的次数
for(i = 0; i < len; i++)
{
c[key[i]]++;
}
//小于或等于数字key[i]的数字出现的次数
for(i = 1; i < 10; i++)
{
c[i] = c[i] + c[i - 1];
}
//b保存每次循环排序的临时结果
for(i = len - 1; i >= 0; i--)
{
b[c[key[i]] - 1] = arr[i];
c[key[i]]--;
}
//将临时结果赋值给原数组再对原数组arr进行排序
memmove(arr, b, sizeof(int) * len);
}
}
基数排序的第二种情况:当待排序元素的位数不完全相同时,上述算法则不适用。
//计算数组arr元素某一位数字存入数组key
int Separate(const int *arr, int *key, int n, int w)
{
int i, d = 1, f = 0;
while(w--)
d *= 10;
//取得数组元素第j位数字并存入数组key中
for(i = 0; i < n; i++)
{
key[i] = arr[i] % d / (d / 10);
if(key[i] != 0)
f = 1;
}
return f; //返回0时排序结束
}
void RadixSort(int *arr, int n)
{
int i, w = 1, c[10];
int *b = new int[n]; //过度数组,保存中间计算结果
int *key = new int[n]; //存储数组元素某一位的数字
while(Separate(arr, key, n, w++))
{
//数组c用来保存第j次循环数组元素第j位数字出现的次数
memset(c, 0, sizeof(c));
//数字key[i]出现的次数
for(i = 0; i < n; i++)
{
c[key[i]]++;
}
//小于或等于数字key[i]的数字出现的次数
for(i = 1; i < 10; i++)
{
c[i] += c[i - 1];
}
//b保存每次循环排序的临时结果
for(i = n - 1; i >= 0; i--)
{
b[c[key[i]] - 1] = arr[i];
c[key[i]]--;
}
//将临时结果赋值给原数组再对原数组arr进行排序
memmove(arr, b, sizeof(int) * n);
}
}
基数排序的时间复杂度为O(d(n+k))。其中d为数组元素最大位数;n为数组大小;k为数组元素每一数位可能取值的种数。
7.堆排序
在介绍堆排序之前,我们需要弄清楚什么是堆?堆可以看作是一棵树,具有以下性质:结点i的孩子结点(如果存在)都不大于(或不小于)该结点。因此堆的根结点的值是堆中所有结点中值最大(或最小)的。利用堆的这个性质我们可以对数组排序,每一趟排序都能找出当前范围内的最大值(或最小值),将其放到正确的位置,最终得到完全有序的数组。因此堆排序过程除了建立堆以外,还需要对不满足堆性质的结点进行调整,使其满足堆性质,即保持堆性质操作。而堆排序的关键也正是如何保持堆性质。
假设对数组a[1..n]调用堆排序,操作如下(以最大堆为例):。
1)保持最大堆性质。对结点i,在元素a[i],a[left]和a[right]中找出最大的,如果结点i的值a[i]是最大的,则以结点i为根的子树满足最大堆性质。否则,将结点i与最大结点交换,从而使结点i满足最大堆性质。
2)建立最大堆。对每个结点调用保持最大堆性质操作,即可建立最大堆。
3)对数组调用堆排序。首先建立最大堆,再从数组尾元素开始至数组首元素为止,将数组首元素与当前元素交换,再对数组首元素到当前元素调用保持最大堆性质操作。最终得到的结果数组即为完全有序的。
//保持最大堆性质是关键
//参数array表示传入的数组
//参数heapSize表示数组中堆的元素个数
//参数i表示需要进行调整的堆的结点
void MaxHeapify(int *array, int heapSize, int i)
{
int largest = 0; //array[i],array[left]和array[right]中最大元素的下标
int left = 2 * i + 1; //结点i左孩子结点的索引
int right = 2 * i + 2; //结点i右孩子结点的索引
//在i结点和其左孩子结点比较找出值较大的一个,并将其索引赋给largest
if(left < heapSize && array[left] > array[i])
{
largest = left;
}
else
{
largest = i;
}
//将i结点和其左右孩子结点进行比较找到值最大的,并将该结点的索引值赋给largest
if(right < heapSize && array[right] > array[largest])
{
largest = right;
}
//如果array[i],array[left]和array[right]中最大的不是i
//则将array[i]与array[largest]交换,然后再对largest结点进行最大堆操作
if(largest != i)
{
int tmp = array[i];
array[i] = array[largest];
array[largest] = tmp;
MaxHeapify(array, heapSize, largest);
}
}
//建立最大堆
void BuildMaxHeap(int *array, int length)
{
//子数组array[length/2..length-1]中的元素都是叶子结点,可以看作只含一个元素的堆
//对树中包含孩子结点的其他结点调用MaxHeapify操作,使得树中的每个结点都满足最大堆性质
for(int i = length / 2 - 1; i >= 0; i--)
{
MaxHeapify(array, length, i);
}
}
//堆排序算法主调程序,传入数据及数组长度,对其进行堆排序
void HeapSort(int *array, int length)
{
//对传入的数组建立最大堆,经过此操作后,结点array[0]是所有元素中值最大的
BuildMaxHeap(array, length);
int i, tmp, heapSize = length;
//从数组最后一个元素开始直到第二个元素,每次将第一个元素与循环的当前元素进行交换
//经过一次循环后得到的i位置上的元素是array[0]..array[i]中最大者
//但是经过元素交换后array[0]不一定满足最大堆性质,所以需要对第一个结点调用MaxHeapify
//使其满足最大堆性质
for(i = length - 1; i > 0; i--)
{
tmp = array[0];
array[0] = array[i];
array[i] = tmp;
heapSize--;
//对第一个结点调用MaxHeapify使其满足最大堆性质
MaxHeapify(array, heapSize, 0);
}
}
堆排序的时间复杂度为O(nlgn),最坏情况Ω(nlgn)。
8.二叉查找树的变形
对二叉查找树的性质稍做修改即用来对数组进行排序:结点x的左子树都不大于它,右子树都大于结点x。这样在对树进行中序遍历可得到一组有序的记录。
struct SortTree //排序树结点结构
{
int data; //结点数据
SortTree *left; //指向左子树
SortTree *right; //指向右子树
//结点初始化
SortTree(int data) : left(NULL), right(NULL)
{
this->data = data;
}
};
// 建立树
void MakeTree(SortTree *&tree, int data)
{
if(tree == NULL) //树为空
{
tree = new SortTree(data); //创建树根结点
}
else
{
SortTree *p = tree;
SortTree *q = tree;
while(q != NULL) //查找元素data插入位置
{
p = q;
(q->data >= data) ? (q = q->left) : (q = q->right);
}
q = new SortTree(data);
(p->data >= data) ? (p->left = q) : (p->right = q);
}
}
运行结果如下:
算法时间复杂度为O(nlgn)。