【Algorithm】Sort Algorithm

排序是我们日常生活处处都是在使用的一种数据处理方法,它是将一组数据按照某种逻辑顺序重新排列的过程,我们比较熟悉的各种游戏中的排名机制其实就是一种排序。常见的排序算法可以笼统地分为两类:比较排序与非比较排序。前者包含我们耳熟能详的冒泡排序、快速排序以及堆排序等;后者中则包含计数排序、桶排序以及基数排序等另一种思路的排序算法。我们会结合理论与实际,来介绍经典的排序算法,我们将关注点放在算法的思想以及其特性上,而非仅仅是实现。

选择排序SelectionSort.

选择排序属于初级排序算法的一种,其思想很简单直观,甚至可以说是暴力。遍历整个数据序列,找到当前序列中最小的一个元素,将其放在最终返回有序序列的第一位。从数据序列中删去上一轮找到的元素,再进行一次遍历,将得到的结果放在有序序列的第二位,依此类推,最终可以得到一个有序序列。关于每一次找出最小元素之后,是选择在原始数据序列中进行交换,还是另外开辟一个序列空间直接摆放,取决于个人具体的实现选择。选择排序的核心思想就在于每一轮遍历,都要找出一个当前的最小元素,也可以认为是每一轮遍历都确定了一个元素的最终位置。选择排序的执行过程很容易想象,它有着一些很鲜明的特点:

  • **运行时间与输入无关:**这并不是说选择排序的运行时间和输入规模无关,虽然这个表述会让人误解。这个性质表示的是:一个已经有序的数据序列和一个随机排列的数据序列,它们进行选择排序的时间几乎一样长。这也就意味着选择排序并不回去利用输入实例的初始状态来进行一些优化,它的前一遍扫描不会为下一遍扫描提供任何信息。
  • **数据移动是最少的:**选择排序每次遍历都会交换两个元素,因此在N次交换之后,整个最终的语序序列就完成了,交换次数与序列规模是线性关系,我们后面遇到的其他任何算法都不会具有这一性质,大部分的交换次数与序列规模都是线性对数甚至平方关系。

对于一个规模为N的数据序列,选择排序需要N2/2次的比较和N次交换,这两个值的得到很简单,不再赘述了。下面是选择排序的Java实现,我们首先给出整个排序项目的模板,后续针对不同的排序算法进行不同的sort()方法的实现:

import java.util.ArrayList;
import java.util.Scanner;

public class Sort
{
    public static boolean less(int v,int w)
    {
        return v<w;
    }

    public static void exch(ArrayList<Integer> a,int i,int j)
    {
        int t=a.get(i);
        a.set(i,a.get(j));
        a.set(j,t);
    }

    public static void show(ArrayList<Integer> a)
    {
        for(int i=0;i<a.size();++i)
        {
            System.out.print(a.get(i)+" ");
        }
        System.out.println();
    }

    public static boolean isSorted(ArrayList<Integer> a)
    {
        for(int i=1;i<a.size();++i)
        {
            if(less(a.get(i),a.get(i-1)))
            {
                return false;
            }
        }
        return true;
    }

    public static void Sort(ArrayList<Integer> a)
    {
    
    }

    public static void main(String[] args)
    {
        Scanner in = new Scanner(System.in);
        ArrayList<Integer> input = new ArrayList<>();
        while (in.hasNextInt())
        {
            input.add(in.nextInt());
        }
        Sort(input);
        assert isSorted(input);
        show(input);
    }
}

下面我们给出对于选择排序的算法实现,只需要将SelectionSort()替换模板中没有实现的Sort()即可完成功能,后面我们给出实例及运行结果。

public static void SelectionSort(ArrayList<Integer> a)
    {
        int N=a.size();
        for(int i=0;i<N;++i)
        {
            int min=i;
            for(int j=i+1;j<N;++j)
            {
                if(less(a.get(j),a.get(min)))
                {
                    min=j;
                }
            }
            exch(a,i,min);
        }
    }

在这里插入图片描述

插入排序InsertionSort.

插入排序的过程很像我们打扑克牌的过程,比如我们手上的牌是[10,7,6,4,3],下一张抽到8,我们就会将其插入10和7之间。计算机进行的插入排序也类似这个过程,只是在插入的时候需要将后续的元素右移以腾出空间。插入排序的每一次执行,都使得索引左边的有序序列规模+1,但需要注意的是,插入序列并没有确定下来它们的最终位置,这一点和选择排序不同。插入排序和选择排序不同的点还在于,插入排序的运行时间受到输入序列的影响,比如一个基本有序的序列和一个随机分布的序列相比,前者的运行时间会比后者快得多。我们这里也对于什么叫做部分有序,做一个简单介绍:

  • 数组中每个元素的位置距离其最终位置都不远;
  • 一个有序的大规模数组接一个小规模数组;
  • 数组中只有几个元素的位置不正确

再次说明,插入排序对于这样的部分有序数组真的很有效,当元素倒置的情况很少时,它可能会比我们后面要介绍的所有高级排序算法都要快。

对于随机排列的长度为N且数据值不重复的数据序列来说,平均情况下需要N2/4次比较和N2/4次交换,最坏情况下需要N2/2次比较和N2/2次交换,最好情况下需要N-1次比较和0次交换。
插入排序所需要的交换操作和数据序列中的倒置数量相同,需要的比较次数大于等于倒置的数量,小于等于倒置的数量加上数据序列的大小减一。这里的倒置就是逆序对的概念,线性代数中曾经涉及,比如说序列523,逆序对有5-2、5-3(按照升序来说,这是逆序),所以其倒置的数量是2.

插入排序在面对随机分布的数据时表现并不好,但一旦数据基本有序(可能是某些类型的非随机数据),插入排序的表现堪称神勇,运行时间直逼线性(而选择排序依旧是雷打不动的平方级)。下面是插入排序的Java代码实现:

    public static void InsertionSort(ArrayList<Integer> a)
    {
        int N=a.size();
        for(int i=1;i<N;++i)
        {
            for(int j=i;j>0 && less(a.get(j),a.get(j-1));--j)
            {
                exch(a,j,j-1);
            }
        }
    }

在这里插入图片描述

希尔排序ShellSort.

和前面的选择排序、插入排序不同,Shell是一个人的名字,所以这个算法名本身并没有给予我们关于它是如何进行排序的信息。希尔排序的思想是基于插入排序的,插入排序之所以对于大规模的乱序数据表现很差,是因为它只会交换相邻的元素,因此乱序的元素只能一点一点地从数据序列的一段移动到另一端。举个极端的例子:最小的那一个数据正好在数据序列的最右端,要将其挪到正确的位置就需要N-1次交换。Shell排序为了改善对于乱序数组的表现,采用交换不相邻元素的措施来实现对数据序列的局部排序,并最终利用插入排序将局部有序的数组进行排序。
Shell排序的具体思想是:使数据序列中任意间隔为h的元素序列都是有序的,这样的数据序列被称为h有序序列,换言之,h有序序列就是一个由h个相互独立的有序序列"编织"在一起的一个大序列。在进行Shell排序时,如果h的值很大,我们就能够实现相距很远的元素间的交换(如果它们有这个需要的话),从而为h值很小时的情况铺平道路,pave the way. 最终我们将h的值减小到1,也就是对于一个已经部分有序的序列进行插入排序,我们说过,表现神勇。希尔排序高效的原因在于它权衡了子数组的规模和有序性。排序之初,h的值很大,子数组很小;排序之后子数组又都是部分有序的,这两种情况都是插入排序最适合的情况,所以希尔排序实际上是在很巧妙地利用插入排序。
关于h的取值问题,我们一般会采用一个递增序列。首先针对给定的序列规模,找到该递增序列中能够取到的最大h,而后开始进行排序,令h从h m a x _{max} 逐步递减到1,整个排序过程就完成了。希尔排序的每一次执行,都是一个以h为步长的插入排序。下面是希尔排序的Java实现:

public static void ShellSort(ArrayList<Integer> a)
    {
        int N=a.size();
        int h=1;
        while (h<N/3)
        {
            h=3*h+1;
        }
        while (h>=1)
        {
            for(int i=h;i<N;++i)
            {
                for(int j=i;j>=h && less(a.get(j),a.get(j-h));j=j-h)
                {
                    exch(a,j,j-h);
                }
            }
            h=h/3;
        }
    }

在这里插入图片描述
在第一个while循环中,我们计算出了合适的h值,至少保证每一个子数组中都有三个元素(h<N/3),在这一实现中,我们使用的递增序列数学通项为(3k-1)/2,前五项为[1,4,13,40,121]。第二个while循环中就很明显的显现出来,希尔排序的每一步都在做步长为h的插入排序,对比插入排序的代码,这一点很容易看出来。我们需要指出的是,子数组的部分有序程度取决于递增序列的选择,而至于如何选择递增序列,这是一个无法回答的问题。因为算法的性能不仅取决于h,还取决于每个h之间的性质,例如它们的公因子等等。可以确定的是,至今并没有哪一个递增序列是被证明了"最优"的。虽然我们无法证明某一个递增序列是最优的,但证明某一个给定的序列和另一个序列的优劣关系是可能的。
希尔排序完全可以应用于大型数据序列(当然要和堆排序的性能相比,还是有点难),并且数据的规模越大,希尔排序超越插入排序和选择排序就越多。

归并排序MergeSort.

我们在分治法Divide-and-Conquer中介绍过归并排序,与之一起的还有快速排序QuickSort. 我们首先介绍归并排序,它最吸引人的特点在于,可以保证将任意长度N的数据序列进行排序的时间和NlogN成正比,简单地说,就是归并排序在任意情况下的时间复杂度都是O(NlogN)。为此付出的是需要使用额外的空间,空间大小与N呈线性关系,space-for-time,很值得。
归并排序最核心的操作在于将两个有序的数组归并成一个,这一操作的步骤也很简单:

  • 为两个子数组分别设置一个索引,初始都指向第一个元素;
  • 比较索引指向元素的大小,将小的一个元素填入最终的数据序列,并增加这一个子数组的索引。在一个子数组没有被耗尽前,重复步骤2;
  • 将未被耗尽的子数组元素追加到最终数据序列之后。

下面是归并排序的Java代码实现:

 public static void Merge(ArrayList<Integer> a,int low,int mid,int high)
    {
        int i=low;
        int j=mid+1;
        ArrayList<Integer> temp=new ArrayList<>();
        for(int x=0;x<a.size();++x)
        {
            temp.add(0);
        }
        for(int k=low;k<=high;++k)
        {
            temp.set(k,a.get(k));
        }
        for(int k=low;k<=high;++k)
        {
            if(i>mid)
            {
                a.set(k,temp.get(j));
                ++j;
            }
            else if(j>high)
            {
                a.set(k,temp.get(i));
                ++i;
            }
            else if(less(temp.get(i),temp.get(j)))
            {
                a.set(k,temp.get(i));
                ++i;
            }
            else
            {
                a.set(k,temp.get(j));
                ++j;
            }
        }
    }

    public static void MergeSort(ArrayList<Integer> a)
    {
        MergeSort(a,0,a.size()-1);
    }

    public static void MergeSort(ArrayList<Integer> a,int beg,int end)
    {
        if(beg>=end)
        {
            return;
        }
        int mid=(beg+end)/2;
        MergeSort(a,beg,mid);
        MergeSort(a,mid+1,end);
        Merge(a,beg,mid,end);
    }

在这里插入图片描述
上述代码中出于希望和前面的排序算法保持相同的调用形式,定义了MergeSort()的重载版本。

快速排序QuickSort.

快速排序可能是应用最广泛的排序算法了,这不仅仅因为它的实现简单,快速排序适用于各种不同的输入数据,并且在一般的应用中都被其他的排序算法快得多。快速排序被誉为20世纪科学与工程领域的十大算法之一,其吸引人的地方在于它是原地排序(不需要很大的额外空间),并且对于长度为N的数组的排序时间和NlogN成正比。我们前面提到过的算法,都无法同时做到这两条。选择排序和插入排序的时间复杂度已经到了N2级别,而归并排序又需要N大小的额外空间。但快速排序的缺点在于它非常脆弱,已经有无数的例子显示多种错误都可以让快速排序实际上的性能恶化到N2级别(例如每一次分组都只分为1和N-1两组).
快速排序的思想是将一个数据序列分成两个子序列,并对两部分分别排序。分割的依据是找到一个枢轴量,其右边的数据都不小于它,而左边的数据都不大于它。快速排序与归并排序的算法思想是互补的,归并排序对于两个子序列分别排序,而后将其归并起来成为最终的有序序列;而快速排序则是在子数据序列分别有序时,整个数据序列就有序了。前者的递归发生在处理整个数据序列之前,而后者的递归发生在处理整个数据序列之后。
快速排序的核心在于分割,这个操作完成后的数组应该满足这样三个条件:

  • 对于该枢轴量pivot,它此时的位置j已经是最终位置;
  • 对于a[low]到a[j-1]的数据而言,它们都不大于pivot;
  • 对于a[j+1]到a[high]的数据而言,它们都不小于pivot。

分割的一般实现策略是这样的,我们不妨选取a[low]作为枢轴量pivot,我们先从左向右扫描,找到一个不小于pivot的元素;再从右向左扫描,找到一个不大于pivot的元素,交换这两个元素的位置(显然需要执行这一步),直到左指针与右指针碰到了一起,我们将pivot和指针指向的该元素交换即可。
快速排序的Java实现如下:

public static int Partition(ArrayList<Integer> a,int low,int high)
    {
        int i=low;
        int j=high;
        int pivot=a.get(low);
        while(true)
        {
            while(less(a.get(i),pivot))
            {
                ++i;
                if(i==high)
                {
                    break;
                }
            }
            while (less(pivot,a.get(j)))
            {
                --j;
                if(j==low)
                {
                    break;
                }
            }
            if(i>=j)
            {
                break;
            }
            exch(a,i,j);
        }
        exch(a,low,j);
        return j;
    }

    public static void QuickSort(ArrayList<Integer> a)
    {
        QuickSort(a,0,a.size()-1);
    }

    public static void QuickSort(ArrayList<Integer> a,int low,int high)
    {
        if(low>=high)
        {
            return;
        }
        int pivot=Partition(a,low,high);
        QuickSort(a,low,pivot-1);
        QuickSort(a,pivot+1,high);
    }

在这里插入图片描述

堆排序HeapSort.

计数排序CountingSort.

冒泡排序BubbleSort.

基数排序RadixSort.

猜你喜欢

转载自blog.csdn.net/weixin_44246009/article/details/106501300