上一篇中简单的回顾了三种比较简单的排序算法:冒泡排序,直接插入排序,简单选择排序,这三种算法的空间复杂度为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))空間
代码
public
void
Sort(
int
[] seq)
{
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。
代码
#region
ISort 成员
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.将另一序列剩下的所有元素直接复制到合并序列尾。
* */
代码
#region
ISort Members
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];
}
}
至此,最基本的几个排序算法介绍完毕,仅看原理自己动手写一下感觉非常不错,欢迎交流。