排序算法小结

https://blog.csdn.net/qq_31116753/article/details/107885765

概述

常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。

比较排序的时间复杂度通常为O(n2)或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。

快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;

当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。

当原表有序或基本有序时,直接插入排序和冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);而快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2);

原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。

排序的稳定性和复杂度

稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。

稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;

不稳定:
  • 选择排序(selection sort)— O(n2)
  • 快速排序(quicksort)— O(nlogn) 平均时间, O(n2) 最坏情况; 对于大的、乱序串列一般认为是最快的已知排序
  • 堆排序 (heapsort)— O(nlogn)
  • 选择排序(selection sort)— O(n2)
  • 希尔排序 (shell sort)— O(nlogn)
  • 基数排序(radix sort)— O(n·k); 需要 O(n) 额外存储空间 (K为特征个数)
稳定:
  • 插入排序(insertion sort)— O(n2)
  • 冒泡排序(bubble sort) — O(n2)
  • 归并排序 (merge sort)— O(n log n); 需要 O(n) 额外存储空间
  • 二叉树排序(Binary tree sort) — O(nlogn); 需要 O(n) 额外存储空间
  • 计数排序 (counting sort) — O(n+k); 需要 O(n+k) 额外存储空间,k为序列中Max-Min+1
  • 桶排序 (bucket sort)— O(n); 需要 O(k) 额外存储空间

选择排序算法准则

每种排序算法都各有优缺点。因此,在实用时需根据不同情况适当选用,甚至可以将多种方法结合起来使用。影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护,一般而言,需要考虑的因素有以下四点:

  • 待排序的记录数目n的大小;
  • 记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小
  • 关键字的结构及其分布情况
  • 对排序稳定性的要求
  • 当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。

    快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;

    堆排序:如果内存空间允许且要求稳定性的

    归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。

  • 当n较大,内存空间允许,且要求稳定性 推荐 归并排序
  • 当n较小,可采用直接插入或直接选择排序。

    直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。

    直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序

  • 一般不使用或不直接使用传统的冒泡排序。
  • 基数排序它是一种稳定的排序算法,但有一定的局限性

    关键字可分解

    记录的关键字位数较少,如果密集更好

    如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。

排序算法动画演示

各种排序

插入排序法

对于前面i个已经升序排好的部分,取挨着的第i+1个,和第i个比较,如果大于的话,第i+1个排序完成;如果是小于,则继续与第i-1个进行比较,直到遇到比其大的,在其后面插入,完成第i+1排序。继续进行第i+2个排序

插入排序基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。插入排序的基本思想是:每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。

当待排序的数据基本有序时,插入排序的效率比较高,只需要进行很少的数据移动。所以插入排序是稳定的。


        void insert_Sort(int[] array)
        {
            for (int i = 1; i < array.Length; i++)
            {
                int data = array[i];
                for (int j = i - 1; j >= 0; j--)
                {
                    if (data < array[j])
                    {
                        array[j + 1] = array[j];
                        array[j] = data;
                    }
                    else
                    {
                        array[j + 1] = data;
                        break;
                    }

                }
            }
        }
             
下面和上面是等价的,因为上内存循环判跳出后,j是又减掉1的

        void insert_Sort2(int[] array)
        {
            for (int i = 1; i < array.Length; i++)
            {
                int data = array[i];
                int j = i - 1;
                for (;j >= 0 && (data < array[j]); j--)
                {
                    array[j + 1] = array[j];
                }
                array[j + 1] = data;
            }
        }
             

冒泡排序法

冒泡排序法是相邻的两个进行比较,大的排在后面,滚动式向后移动比较,经过第一轮比较和移动,最大元素会排在最后面,第二轮后,次大的位于倒数第二个,依次进行。

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端。


        void bubble_Sort(int[] array)
        {
            for (int i = 0; i < array.Length-1; i++)
            {
                for (int j = 1; j < array.Length-i; j++)
                {
                    if (array[j] < array[j-1])
                    {
                        int tmp = array[j];
                        array[j] = array[j-1];
                        array[j-1] = tmp;
                    }
                }
            }       
        }
             
优化一

如果某一轮两两比较中没有任何元素交换,这说明已经都排好序了,算法结束,可以使用一个Flag做标记,默认为false,如果发生交互则置为true,每轮结束时检测Flag,如果为true则继续,如果为false则返回。


        void bubble_Sort_Optimize1(int[] array)
        {
            bool flag = false;
            for (int i = 0; i < array.Length - 1;flag = false, i++)
            {
                for (int j = 1; j < array.Length - i; j++)
                {
                    if (array[j] < array[j - 1])
                    {
                        int tmp = array[j];
                        array[j] = array[j - 1];
                        array[j - 1] = tmp;
                        flag = true;
                    }
                }
                if (!flag)
                {
                    break;
                }
            }
        }
             
优化二

某一轮结束位置为j,但是这一轮的最后一次交换发生在lastSwap的位置,则lastSwap到j之间是排好序的,下一轮的结束点就不必是j--了,而直接到lastSwap即可,代码如下:


        void bubble_Sort_Optimize2(int[] array)
        {
            int lastSwap = array.Length;
            for (int i = 0; i < array.Length - 1; i++)
            {
                int j = 1;
                for (; j < lastSwap; j++)
                {
                    if (array[j] < array[j - 1])
                    {
                        int tmp = array[j];
                        array[j] = array[j - 1];
                        array[j - 1] = tmp;                      
                    }
                }
                lastSwap = j-1;
            }
        }
             

选择排序法

遍历数组,遍历到i时,a0,a1...ai-1是已经排好序的,然后从i到n选择出最小的,记录下位置,如果不是第i个,则和第i个元素交换。此时第i个元素可能会排到相等元素之后,造成排序的不稳定。


        void selection_Sort(int[] array)
        {
            for (int i = 0; i < array.Length - 1; i++)
            {
                int pos = i;
                for (int j = i + 1; j < array.Length; j++)
                    if (array[pos] > array[j])
                    {
                        pos = j;
                    }
                if (pos != i)
                {
                    int tmp = array[i];
                    array[i] = array[pos];
                    array[pos] = tmp;
                }
            }
        }
             

快速排序法

快速排序(Quicksort)是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。其实现时通过递归实现的(也可以不用递归)

该方法的基本思想是:

  • 先从数列中取出一个数作为基准数。
  • 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
  • 再对左右区间重复第二步,直到各区间只有一个数。

        void QuickSort(int[] R, int low, int high)
        {
            int pivotLoc = 0;

            if (low < high)
            {
                pivotLoc = Partition(R, low, high);
                QuickSort(R, low, pivotLoc - 1);
                QuickSort(R, pivotLoc + 1, high);
            }
        }


        int Partition(int[] R, int low, int high)
        {
            int temp = R[low];
            while (low < high)
            {
                while (low < high && temp <= R[high])
                {
                    high--;
                }
                R[low] = R[high];
                while (low < high && temp >= R[low])
                {
                    low++;
                }
                R[high] = R[low];
            }
            R[low] = temp;
            return low;
        }
             

堆排序法

堆排序是一种树形选择排序,是对直接选择排序的改进。

我们来看看什么是堆(heap):

  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆总是一棵完全二叉树(Complete Binary Tree)。

完全二叉树是由满二叉树(Full Binary Tree)而引出来的。除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树称为满二叉树。

如果除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺少右边的若干结点,这样的二叉树被称为完全二叉树。

堆排序是把数组看作堆,堆排序的第一步是建堆,然后是取堆顶元素然后调整堆。建堆的过程是自底向上不断调整达成的,这样当调整某个结点时,其左节点和右结点已经是满足条件的,此时如果两个子结点不需要动,则整个子树不需要动,如果调整,则父结点交换到子结点位置,再以此结点继续调整。

初始化堆

当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆

调整堆成最大堆

堆排序

  • 堆排序就是把堆顶的最大数取出,如上16删掉
  • 将剩余的堆继续调整为最大堆,具体过程在第二块有介绍,以递归实现
  • 剩余部分调整为最大堆后,再次将堆顶的最大数取出,再将剩余部分调整为最大堆,这个过程持续到剩余数只有一个时结束

        //堆排序算法(传递待排数组名,即:数组的地址。故形参数组的各种操作反应到实参数组上)
        private static void HeapSortFunction(int[] array)
        {
            try
            {
                BuildMaxHeap(array);    //创建大顶推(初始状态看做:整体无序)
                for (int i = array.Length - 1; i > 0; i--)
                {
                    Swap(ref array[0], ref array[i]); //将堆顶元素依次与无序区的最后一位交换(使堆顶元素进入有序区)
                    MaxHeapify(array, 0, i); //重新将无序区调整为大顶堆
                }
            }
            catch (Exception ex)
            { }
        }

        
        // 创建大顶推(根节点大于左右子节点)
        //array待排数组
        private static void BuildMaxHeap(int[] array)
        {
            try
            {
                //根据大顶堆的性质可知:数组的前半段的元素为根节点,其余元素都为叶节点
                for (int i = array.Length / 2 - 1; i >= 0; i--) //从最底层的最后一个根节点开始进行大顶推的调整
                {
                    MaxHeapify(array, i, array.Length); //调整大顶堆
                }
            }
            catch (Exception ex)
            { }
        }

    
        // 大顶推的调整过程
        //array待调整的数组
        //currentIndex待调整元素在数组中的位置(即:根节点)
        //heapSize堆中所有元素的个数
        private static void MaxHeapify(int[] array, int currentIndex, int heapSize)
        {
            try
            {
                int left = 2 * currentIndex + 1;    //左子节点在数组中的位置
                int right = 2 * currentIndex + 2;   //右子节点在数组中的位置
                int large = currentIndex;   //记录此根节点、左子节点、右子节点 三者中最大值的位置

                if (left < heapSize && array[left] > array[large])  //与左子节点进行比较
                {
                    large = left;
                }
                if (right < heapSize && array[right] > array[large])    //与右子节点进行比较
                {
                    large = right;
                }
                if (currentIndex != large)  //如果 currentIndex != large 则表明 large 发生变化(即:左右子节点中有大于根节点的情况)
                {
                    Swap(ref array[currentIndex], ref array[large]);    //将左右节点中的大者与根节点进行交换(即:实现局部大顶堆)
                    MaxHeapify(array, large, heapSize); //以上次调整动作的large位置(为此次调整的根节点位置),进行递归调整
                }
            }
            catch (Exception ex)
            { }
        }

    
        // 交换函数
        //a:元素a
        //b:元素b
        private static void Swap(ref int a, ref int b)
        {
            int temp = 0;
            temp = a;
            a = b;
            b = temp;
        }
             

堆排序的时间复杂度为O(nlogn)。由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然要远远好过于冒泡、简单选择、直接插入的O(n2)的时间复杂度了。空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错。不过由于记录的比较与交换是跳跃式进行,因此堆排序是一种不稳定的排序方法。

归并排序

归并排序(Merge Sort)是利用"归并"技术来进行排序。归并是指将若干个已排序的子文件合并成一个有序的文件。

分治法的三个步骤
  • 分解:将当前区间一分为二,即求分裂点
  • 求解:递归地对两个子区间R[low..mid]和R[mid+1..high]进行归并排序
  • 组合:将已排序的两个子区间R[low..mid]和R[mid+1..high]归并为一个有序的区间R[low..high]
  • 递归的终结条件:子区间长度为1(一个记录自然有序)。

归并排序是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。这需要将待排序序列中的所有记录扫描一遍,因此耗费O(n)时间,而由完全二叉树的深度可知,整个归并排序需要进行.logn.次,因此,总的时间复杂度为O(nlogn)。

归并算法需要两两比较,不存在跳跃,因此归并排序是一种稳定的排序算法。


         //归并排序(目标数组,子表的起始位置,子表的终止位置)
        private static void MergeSortFunction(int[] array, int first, int last)
        {
            try
            {
                if (first < last)   //子表的长度大于1,则进入下面的递归处理
                {
                    int mid = (first + last) / 2;   //子表划分的位置
                    MergeSortFunction(array, first, mid);   //对划分出来的左侧子表进行递归划分
                    MergeSortFunction(array, mid + 1, last);    //对划分出来的右侧子表进行递归划分
                    MergeSortCore(array, first, mid, last); //对左右子表进行有序的整合(归并排序的核心部分)
                }
            }
            catch (Exception ex)
            { }
        }

        //归并排序的核心部分:将两个有序的左右子表(以mid区分),合并成一个有序的表
        private static void MergeSortCore(int[] array, int first, int mid, int last)
        {
            try
            {
                int indexA = first; //左侧子表的起始位置
                int indexB = mid + 1;   //右侧子表的起始位置
                int[] temp = new int[last + 1]; //声明数组(暂存左右子表的所有有序数列):长度等于左右子表的长度之和。
                int tempIndex = 0;
                while (indexA <= mid && indexB <= last) //进行左右子表的遍历,如果其中有一个子表遍历完,则跳出循环
                {
                    if (array[indexA] <= array[indexB]) //此时左子表的数 <= 右子表的数
                    {
                        temp[tempIndex++] = array[indexA++];    //将左子表的数放入暂存数组中,遍历左子表下标++
                    }
                    else//此时左子表的数 > 右子表的数
                    {
                        temp[tempIndex++] = array[indexB++];    //将右子表的数放入暂存数组中,遍历右子表下标++
                    }
                }
                //有一侧子表遍历完后,跳出循环,将另外一侧子表剩下的数一次放入暂存数组中(有序)
                while (indexA <= mid)
                {
                    temp[tempIndex++] = array[indexA++];
                }
                while (indexB <= last)
                {
                    temp[tempIndex++] = array[indexB++];
                }

                //将暂存数组中有序的数列写入目标数组的制定位置,使进行归并的数组段有序
                tempIndex = 0;
                for (int i = first; i <= last; i++)
                {
                    array[i] = temp[tempIndex++];
                }
            }
            catch (Exception ex)
            { }
        }
             

希尔排序

希尔排序的实质就是分组插入排序,该方法又称缩小增量排序

希尔排序的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。

希尔排序是对插入排序的优化,基于以下两个认识:1. 数据量较小时插入排序速度较快,因为n和n2差距很小;2. 数据基本有序时插入排序效率很高,因为比较和移动的数据量少。


        void shell_Sort(int[] array)
        {
            int d, i, j, temp; //d为增量
            for (d = array.Length / 2; d >= 1; d = d / 2) //增量递减到1使完成排序
            {
                for (i = d; i < array.Length; i++)   //插入排序的一轮
                {
                    temp = array[i];
                    for (j = i - d; (j >= 0) && (array[j] > temp); j = j - d)
                    {
                        array[j + d] = array[j];
                    }
                    array[j + d] = temp;
                }
            }
        }

            //下为插入法  可以看出是把里面的1变成了增量d
             for (int i = 1; i < array.Length; i++)
            {
                int data = array[i];
                int j = i - 1;
                for (;j >= 0 && (data < array[j]); j--)
                {
                    array[j + 1] = array[j];
                }
                array[j + 1] = data;
            }
             

计数排序法

计数排序的思想是,考虑待排序数组中的某一个元素a,如果数组中比a小的元素有s个,那么a在最终排好序的数组中的位置将会是s+1,如何知道比a小的元素有多少个,肯定不是通过比较去觉得,而是通过数字本身的属性,即累加数组中最小值到a之间的每个数字出现的次数(未出现则为0),而每个数字出现的次数可以通过扫描一遍数组获得。

计数排序要求待排序的数组元素都是 整数,有很多地方都要去是0-K的正整数,其实负整数也可以通过都加一个偏移量解决的。

计数排序适合数据分布集中的排序,如果数据太分散,会造成空间的大量浪费

计数排序的步骤:

  • 找出待排序的数组中最大和最小的元素(计数数组C的长度为max-min+1,其中位置0存放min,依次填充到最后一个位置存放max)
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1(反向填充是为了保证稳定性)

桶排序法

假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。

桶排序利用函数的映射关系,减少了计划所有的比较操作,是一种Hash的思想,可以用在海量数据处理中。

计数排序也可以看作是桶排序的特例,数组关键字范围为N,划分为N个桶。

基数排序法

是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数

它是这样实现的:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

基数排序也可以看作一种桶排序,不断的使用不同的标准对数据划分到桶中,最终实现有序。基数排序的思想是对数据选择多种基数,对每一种基数依次使用桶排序。

基数排序的步骤:以整数为例,将整数按十进制位划分,从低位到高位执行以下过程。

  • 从个位开始,根据0~9的值将数据分到10个桶桶,例如12会划分到2号桶中。
  • 将0~9的10个桶中的数据顺序放回到数组中。
  • 重复上述过程,一直到最高位。
  • 上述方法称为LSD(Least significant digital),还可以从高位到低位,称为MSD。LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。

        public int[] RadixSort(int[] ArrayToSort, int digit)
        {
            //low to high digit 
            for (int k = 1; k <= digit; k++)
            {
                //temp array to store the sort result inside digit 
                int[] tmpArray = new int[ArrayToSort.Length];
                //temp array for countingsort 
                int[] tmpCountingSortArray = new int[10] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
                //CountingSort         
                for (int i = 0; i < ArrayToSort.Length; i++)
                {
                    //split the specified digit from the element 
                    int tmpSplitDigit = ArrayToSort[i] / (int)Math.Pow(10, k - 1) - (ArrayToSort[i] / (int)Math.Pow(10, k)) * 10;
                    tmpCountingSortArray[tmpSplitDigit] += 1;
                }
                for (int m = 1; m < 10; m++)
                {
                    tmpCountingSortArray[m] += tmpCountingSortArray[m - 1];
                }
                //output the value to result     
                for (int n = ArrayToSort.Length - 1; n >= 0; n--)
                {
                    int tmpSplitDigit = ArrayToSort[n] / (int)Math.Pow(10, k - 1) - (ArrayToSort[n] / (int)Math.Pow(10, k)) * 10;
                    tmpArray[tmpCountingSortArray[tmpSplitDigit] - 1] = ArrayToSort[n];
                    tmpCountingSortArray[tmpSplitDigit] -= 1;
                }
                //copy the digit-inside sort result to source array     
                for (int p = 0; p < ArrayToSort.Length; p++)
                {
                    ArrayToSort[p] = tmpArray[p];
                }
            }
            return ArrayToSort;
        }
             
posted @ 2017-05-11 11:04  凯帝农垦  阅读(866)  评论(1编辑  收藏  举报