zoukankan      html  css  js  c++  java
  • 数据结构之排序算法二:堆排序,快速排序,归并排序

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

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

  • 相关阅读:
    sh脚本学习笔记
    idea常见快捷键
    linux操作命令笔记
    【题解】[国家集训队]圈地计划
    【题解】[国家集训队]happiness
    【题解】小M的作物
    cpu的MMU
    socat命令
    strace命令
    Linux的文件描述符
  • 原文地址:https://www.cnblogs.com/millen/p/1682612.html
Copyright © 2011-2022 走看看