上一篇中简单的回顾了三种比较简单的排序算法:冒泡排序,直接插入排序,简单选择排序,这三种算法的空间复杂度为O(1),时间复杂度为O(N2)。这次我们来看看相对复杂的排序算法,前面介绍的排序算法并没有保存比较结果,导致重复比较,下面介绍的三种排序算法都会将比较结果保存下来,所以时间复杂度会相对低,包括快速排序,堆排序,归并排序(二路归并)。
快速排序原理:
* 快速排序(Quicksort)是对冒泡排序的一种改进。由C. A. R. Hoare在1962年提出。
* 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,
* 然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
[编辑本段]算法过程
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用第一个数据)作为关键数据,
* 然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。
* 一趟快速排序的算法是:
1)设置两个变量low、high,排序开始的时候:low=1,high=N-1;
2)以第一个数组元素作为关键数据,赋值给X,即 X=A[0];
3)从high开始向前搜索,即由后开始向前搜索(high=high-1),找到第一个小于X的值,
* 让该值与X交换(找到就行.找到后low大小不变);
4)从low开始向后搜索,即由前开始向后搜索(low=low+1),找到第一个大于X的值,
* 让该值与X交换(找到就行.找到后high大小不变);
5)重复第3、4步,直到 low=high; (3,4步是在程序中没找到时候high=high-1,low=low+1。找到并交换的时候low,
* high指针位置不变。另外当low=high这过程一定正好是low+或high+完成的最后,循环结束)
例如:待排序的数组A的值分别是:(初始关键数据:X=49) 注意关键X永远不变.
* 永远是和X进行比较 无论在什么位置 最后的目的就是把X放在中间小的放前面大的放后面
A[0] 、 A[1]、 A[2]、 A[3]、 A[4]、 A[5]、 A[6]:
49 38 65 97 76 13 27
进行第一次交换后: 27 38 65 97 76 13 49
( 按照算法的第三步从后面开始找)
进行第二次交换后: 27 38 49 97 76 13 65
( 按照算法的第四步从前面开始找>X的值,65>49,两者交换,此时:low=3 )
进行第三次交换后: 27 38 13 97 76 49 65
( 按照算法的第五步将又一次执行算法的第三步从后开始找
进行第四次交换后: 27 38 13 49 76 97 65
( 按照算法的第四步从前面开始找大于X的值,97>49,两者交换,此时:high=4 )
此时再执行第三步的时候就发现low=high,从而结束一趟快速排序,那么经过一趟快速排序之后的结果是:27 38 13 49 76 97 65,即所以大于49的数全部在49的后面,所以小于49的数全部在49的前面。
* 时间复杂度:
* 快速排序在最好情况下为O(nlog(2)(n)),此时待排序的数列每次都可以划分成等大小的两个数列,这样按根分解次数形成一个完全二叉树。
* 最坏情况为O(n∧2),此时待排序的数列已经排好序,这样按根分解次数形成一个单支二叉树。
空间复杂度:
* O(log(2)(n))空間
{
Quick_Sort(seq, 0, seq.Length - 1);
}
//采用原地快速排序
private void Quick_Sort(int[] seq, int low, int high)
{
int tmp = seq[low];
int i = low;
int j = high;
//一趟排序
while (low < high)
{
while (low < high)
{
if (seq[high] < tmp)
{
seq[low] = seq[high];
seq[high] = tmp;
low++;
break;
}
else
{
high--;
}
}
while (low < high)
{
if (seq[low] > tmp)
{
seq[high] = seq[low];
seq[low] = tmp;
high--;
break;
}
else
{
low++;
}
}
}
//此时low=high,对seq中由low和high分拆的两边分别递归调用
if (i < low - 1)
{
Quick_Sort(seq, i, low - 1);
}
if (j > high + 1)
{
Quick_Sort(seq, high + 1, j);
}
}
堆排序原理:
/* “堆”定义
n个关键字序列Kl,K2,…,Kn称为(Heap),当且仅当该序列满足如下性质(简称为堆性质):
(1) ki≤K2i且ki≤K2i+1
* 或
(2)ki≥Kn2i且ki≥K2i+1(1≤i≤ n)
若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:
* 树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
* (即如果按照线性存储该树,可得到一个不下降序列或不上升序列)
*
*
*
* 算法分析
堆[排序的时间,主要由建立初始]堆和反复重建堆这两部分的时间开销构成。
堆排序的最坏时间复杂度为O(nlog2n)。堆序的平均性能较接近于最坏性能。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
堆排序是就地排序,辅助空间为O(1),
它是不稳定的排序方法。
*
* 算法步骤:
* 1)将输入的顺序表视为按顺序表存储的完全二叉树。
* 2)将完全二叉树调整为堆。
*
* 附需用到的顺序存储完全二叉树性质:
* 有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:
若I为结点编号则
如果I<>1,则其父结点的编号为I/2;
如果2*I<=N,则其左儿子(即左子树的根结点)的编号为2*I;若2*I>N,则无左儿子;
如果2*I+1<=N,则其右儿子的结点编号为2*I+1;若2*I+1>N,则无右儿子。
*
*
*
* 二叉树的性质
性质1 满二叉树定理:非空二叉树树叶的数目等于其分支结点数加1。
性质2 满二叉树定理推论:一个非空二叉树的空子树数目等于其结点数加1。
性质3 任何一棵二叉树,度为0的结点比度为2的结点多一个。
性质4 二叉树的第i层(根为第0层,i≥0)最多有2i次方个结点。
性质5 高度为k的二叉树至多有2k-1个结点。
性质6 有n个结点(n>0)的完全二叉树的高度为log2(n+1), 深度为 log2(n+1)-1。
public void Sort(int[] seq)
{
//1.取最大节点为已排好序节点开始建立堆
Heap_Sort(seq, seq.Length - 1, seq.Length - 1);
for (int i = seq.Length - 1; i >= 0;i-- )
{
//2.从已建好的堆中取出顶点与堆尾元素交换
int tmp = seq[0];
seq[0] = seq[i];
seq[i] = tmp;
//3.将此时队列视为除队列最后一个元素外顶点为seq[0](除根节点外左右子树已为堆)的新队列,
//从堆顶重建即可,(相比简单选择排序保留中间的比对结果,减少比对次数)
Heap_Sort(seq, 0, i-1);
}
}
#endregion
#region 采用最大堆排序,节点排序方法
/// <summary>
/// 采用最大堆排序,节点排序方法,形成以该节点为顶点的堆。
/// 具体步骤为:
/// 1.先判断待排序节点有无子节点(即有无左子节点)
/// 2.如果有左子节点,给中间变量maxIndex赋值为左子节点索引
/// 3.再判断有无右子节点,如果有,比较左右子节点的值,给maxIndex赋值为较大子节点的索引
/// 4.判断较大子节点的值与当前节点的值,如果较大子节点值大于当前子节点值,则交换
/// 5.将maxIndex值赋给当前节点索引,重复步骤1,2,3,4
/// </summary>
/// <param name="seq">待排序数组</param>
/// <param name="startIndex">该节点为左右子树为堆的待排序节点在数组中的
/// 索引,如果待排序数组完全未排序,则应将待排序数组的最后一个元素视为左右子树已排好序</param>
/// <param name="endIndex">待排序数组中从第一个元素起需排序的元素索引</param>
private void Heap_Sort(int[] seq, int startIndex,int endIndex)
{
//1.待排序节点seq[startIndex],节点编号为startIndex+1
for (int i = startIndex; i >= 0; i--)//从编号为starIndex+1节点逐层遍历二叉树,也可改写为没有父节点就退出的while循环
{
while (2 * (i + 1)<= endIndex+1)//循环退出条件为待调整节点没有左子节点
{
//2.中间变量maxIndex赋值为左子节点索引
int maxIndex = 2 * (i + 1) - 1;
//3.判断是否有右子节点,2(i+1)+1>N则无右节点,
//如果有右子节点seq[2(i+1)+1-1](编号为i+1的右子节点编号为2(i+1)+1),与左子节点比较,
//如果右子节点较大,则maxIndex赋值为右子节点索引
if (2 * (i + 1) + 1 <= endIndex+1 && seq[2 * (i + 1) -1] < seq[2 * (i + 1)])
{
maxIndex = 2 * (i + 1);
}
//4.seq[i]与较大子节点比较大小,如果小于子节点则值交换,并i调整为maxIndex,如果不交换,退出循环
if (seq[i] < seq[maxIndex])
{
int tmp = seq[i];
seq[i] = seq[maxIndex];
seq[maxIndex] = tmp;
i = maxIndex;
}
else
{
break;
}
}
}
}
#endregion
二路归并排序原理:
/* 归并排序其实是属于分治算法,算法思想是:把待排序序列分成相同大小的两个部分,
* 依次对这两部分进行归并排序,完毕之后再按照顺序进行合并.
假设顺序表中有n个记录,把它看成n个长度为1的有序表,
* 从第一个有序表开始,把相邻的两个有序表进行两两合并成一个有序表,
* 得到n/2个长度为2的有序表。如此重复,最后得到一个长度为n的有序表。
归并排序的时间复杂度是O(nlogn),空间复杂度是O(n),
* 单趟的排序思路:
1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并
后的序列。
2.设定两个指针,最初位置分别为两个已经排序序列的起始位置。
3.比较两个指针所指向的元素,选择相对小的元素放入到合并空间,
并移动指针到下一位置。
4.重复步骤3直到某一指针达到序列尾。
5.将另一序列剩下的所有元素直接复制到合并序列尾。
* */
public void Sort(int[] seq)
{
int i = 1;
while (i < seq.Length)
{
//1.按i大小将数组seq分隔成小数组,每相邻的两个数组进行单趟归并,如果有未能分组的不排序
int j=0;
while (j + i - 1 < seq.Length)//判断根据i划分的第一个数组的结束索引未超出数组长度
{
if (j + 2 * i - 1 < seq.Length)//判断根据i划分的第二个数组的结束索引未超出数组长度
{
MergeSortOperate(seq, j, j + i - 1, j + 2 * i - 1);
}
else if (j + i != seq.Length)//判断seq长度为奇数时第一次切割时最后一个元素不分组
{
MergeSortOperate(seq, j, j + i - 1, seq.Length - 1);
}
j+=2*i;
}
//2.i=2i将i翻倍,重复步骤1
i=2*i;
}
}
#endregion
/// <summary>
/// 将数组中指定的位置连续的两个排好序的数组进行合并
/// </summary>
/// <param name="seq">待排序数组</param>
/// <param name="startIndex1">第一个已排好序数组起始位置索引</param>
/// <param name="endIndex1">第一个已排好序数组结束位置索引</param>
/// <param name="endIndex2">第二个已排好序数组结束位置索引</param>
private void MergeSortOperate(int[] seq, int startIndex1, int endIndex1, int endIndex2)
{
int startIndex2 = endIndex1 + 1;
int[] seqTemp = new int[endIndex2 - startIndex1 + 1];
int tmpStartIndex1 = startIndex1;
for (int i = 0; i < seqTemp.Length; ++i)
{
if (startIndex1 <= endIndex1 && startIndex2 <= endIndex2)//判断两个待合并数组是否已经有一个已经插入完成
{
if (seq[startIndex1] < seq[startIndex2])
{
seqTemp[i] = seq[startIndex1];
++startIndex1;
}
else
{
seqTemp[i] = seq[startIndex2];
++startIndex2;
}
}
else
{
if (startIndex1 > endIndex1)//判断seq1是否已经插入完成
{
seqTemp[i] = seq[startIndex2];
++startIndex2;
}
else
{
seqTemp[i] = seq[startIndex1];
++startIndex1;
}
}
}
for (int i = 0; i < seqTemp.Length; ++i)
{
seq[i + tmpStartIndex1] = seqTemp[i];
}
}
至此,最基本的几个排序算法介绍完毕,仅看原理自己动手写一下感觉非常不错,欢迎交流。