数据结构之排序算法二:堆排序,快速排序,归并排序

  上一篇中简单的回顾了三种比较简单的排序算法:冒泡排序,直接插入排序,简单选择排序,这三种算法的空间复杂度为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];
            }
        }

 

至此,最基本的几个排序算法介绍完毕,仅看原理自己动手写一下感觉非常不错,欢迎交流。

posted @ 2010-03-10 15:24  catch22  阅读(2340)  评论(0编辑  收藏  举报