一点就懂的经典十大排序算法

1、概述

十种常见排序算法可以分为两大类

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
    如图所示
    在这里插入图片描述
    一些概念小结
  • 稳定性判断:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。使用大O估计法
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。使用大O估计法

关于稳定性:需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。

2、十大算法性能比较

在这里插入图片描述

3、排序算法精讲

3.1 超级经典的排序——冒泡排序和它的优化

1)原理
n个元素的序列,从第一个数开始,每次比较相邻的两个数,如果符合a[j]<a[j+1],就交换,直到把当前的最大数“冒泡”到最上面,我们完成一轮的冒泡趟数;每一轮冒泡趟数,找到最大的那个数放到数组最后面,然后接着对剩下的n-1个数进行下一轮的冒泡。这样总共要冒泡 n-1趟
2)代码实现(java)

    public static void bubbleSort(int[]array,boolean exchange) {//重载,exchange标志表示为升序排序还是降序排序
        for (int i =1; i < array.length; i++)//控制次数,第几趟排序,只需要n-1趟
        {
            for (int j = 0; j < array.length - i; j++)//i表示进行的趟数,每一趟能排好一个数,只需要排剩下的array.length-i个数
                if (exchange ? array[j] > array[j + 1] : array[j] < array[j + 1])//控制升序还是降序
                {
                    int temp = array[j];
        			array[j]=arrjay[j+1];
        			array[j+1]=temp;//交换两个数 
                }             

        }
    }

3)优化
我们知道每一趟的进行,只是为了把当前的最大的数冒泡上去,那如果当前趟数的最大数本身就是在数组的最右边(即本身已经冒泡好了),那我们何必继续该轮冒泡呢?基于这一点,我们可以推出:上一轮没有发生交换,说明数据的顺序已经排好,没有必要继续进行下去。

解决办法:加一个标志位,记录上一次是否发生了交换,如果是,我们则进行下一轮,如果没有,说明已经冒泡好了

代码实现:

public static void bubbleSort(int[]array,boolean exchange) {//重载,exchange标志表示为升序排序还是降序排序
   boolean flag = true;
   for (int i =1; i < array.length&&flag; i++)//控制次数,第几趟排序,只需要n-1趟,有交换时进行,只有flag=false就说明上一次一个元素都没有进行交换
  {
   	flag = false;//假定未交换
  	for (int j = 0; j < array.length - i; j++)//i表示进行的趟数,每一趟能排好一个数,只需要排剩下的array.length-i个数
  		if (exchange ? array[j] > array[j + 1] : array[j] < array[j + 1])//控制升序还是降序
             {
                int temp = array[j];
        		array[j]=arrjay[j+1];
        		array[j+1]=temp;//交换两个数 
        		flag = true;//记录是否发生交换,
              }             

        }
    }

4)Demo演示
原算法:
在这里插入图片描述
优化以后:
在这里插入图片描述
5)评价
它的时间复杂度达到了 O(N2)。这是一个非常高的时间复杂度。冒泡排序早在 1956 年就有人开始研究,之后有很多人都尝试过对冒泡排序进行改进,但结果却令人失望。如 Knuth(Donald E. Knuth 中文名为高德纳,1974 年图灵奖获得者)所说:“冒泡排序除了它迷人的名字和导致了某些有趣的理论问题这一事实之外,似乎没有什么值得推荐的。” 假如我们的计算机每秒钟可以运行 10 亿次,那么对 1 亿个数进行排序,桶排序则只需要 0.1 秒,而冒泡排序则需要 1 千万秒,达到 115 天之久!有没有既不浪费空间又可以快一点的排序算法呢?那就是“快速排序”啦!

3.2 最常用的排序——快速排序(基准值分段,交换,分而治之,递归实现)

1)原理
设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为基准数据,然后将所有比它小的数都放到它左边所有比它大的数都放到它右边这个过程称为一趟快速排序。值得注意的是,然后我们就相当于将一个数组分成了两截,这个时候我们可以继续下去,对这两个子数组继续划分,而实现这个,可以用递归。我们将大的原始数组分成小的两段,再继续分,1分2,2分4...,体现了分而治之递的思想,而实现上我们利用了递归!

一趟快速排序的算法是:
1)设置两个变量i、j(i,j我们可以看成是两个哨兵,i是从左到右放哨,j是从右到左放哨)排序开始的时候:i=0,j=N-1;
2)每次都是将该数组第一个元素作为基准数据,赋值给base,即base=A[0];
3)从j(哨兵j先走)开始放哨,即由后开始向前搜索(j--),找到第一个小于base的值A[j]每找到一个小于base的值,我们就查看在i<j的条件下(哨兵i开始前进)是否存在比base值大的数A[i]

  • 存在就进行交换swap(A[i],A[j]),然后回跳到哨兵j继续走
  • 不存在,哨兵i就会继续放哨,直到i=j,完成一趟快速排序

最后我们把这个过程递归即可,需要注意的就是分而治之时的基准值都是每一子数组的第一个元素。(如图,基准为6,第一个元素)
在这里插入图片描述
在这里插入图片描述
。。。。。
2)代码实现
这是我自己看一半的原理,然后自己写的:

 public void quickSort(int[]array,int begin,int end){

        if(begin>=0&&end>=0&&begin<end){//递归结束条件
            int i=begin,j=end;
            //以第一个为基准值
           int base = array[begin];
           while(i<j)//哨兵j先移动
            {
                if(array[j]<base)//哨兵j每找到一个小于基准值的
                    for(;i<j;i++){
                        if(array[i]>base){//哨兵i就找一次大于基准值的进行交换,交换一次就回去继续下一轮交换
                            swap(array,i,j);
                            break;
                        }
                        //找不到比基准大的哨兵i继续往右移动。
                    }
                if(i==j)//因为现在i不清楚是从哪里出来的我们需要判断是break出来还是i==j
                    break;
                j--;//否则我们哨兵j继续搜索

            }
            
           swap(array,begin,j);//交换基准值,实现小于基准值的都在左边,大于基准值的都在右边

            //递归
            quickSort(array,0,j-1);//左子数组
            quickSort(array,j+1,end);//右子数组

        }

    }

书本的标准写法:

public static int[] qsort(int arr[],int start,int end) {        
    int pivot = arr[start];        
    int i = start;        
    int j = end;        
    while (i<j) {            
        while ((i<j)&&(arr[j]>pivot)) {                
            j--;            
        }            
        while ((i<j)&&(arr[i]<pivot)) {                
            i++;            
        }            
        if ((arr[i]==arr[j])&&(i<j)) {                
            i++;            
        } else {                
            int temp = arr[i];                
            arr[i] = arr[j];                
            arr[j] = temp;            
        }        
    }        
    if (i-1>start) arr=qsort(arr,start,i-1);        
    if (j+1<end) arr=qsort(arr,j+1,end);        
    return (arr);    
}    

3)Demo演示
我的:

 public static void main(String[]args){
        int[] array = new int[]{1,2,5,5,6,6,0,0,1,2,5,55,555,7777};
        for(int arr:array){
            System.out.print(arr+" ");
        }
        System.out.println();
        new QuickSort().quickSort(array);
        for(int arr:array){
            System.out.print(arr+" ");
        }

在这里插入图片描述
4)评价
从上面的结果就能知道快速排序是不稳定的,原因也很容易想,这里就不赘述。快速排序的一次划分算法从两头交替搜索,直到i和j重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)。
可以证明快速排序的平均时间复杂度也是O(nlog2n)。因此,该排序方法被认为是目前最好的一种内部排序方法。
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1);但最坏的情况下,栈的最大深度为n。这样,快速排序的空间复杂度为O(log2n))

3.3 最简单直接的排序——直接选择排序(挑最大/最小的那个)

1)原理

它的工作原理是:第一次从待排序的n个数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的n-1个未排序元素中寻找到最小(大)元素,然后放到当前的序列的起始位置。以此类推,直到全部待排序的数据元素的个数为零。趟数是n-1趟。是不是很简单啊?我们每次都只用找最小的一个数,然后把它从左到右依次放就好了。
2)代码实现

  public static void selectSort(int[]array){
        for(int i=0;i<array.length;i++){
            int min= i;//假定当前的序列第一个数就是最小的值
            for(int j=i+1;j<array.length;j++){
                if(array[j]<array[min])
                    min = j;//每次我们从剩下的数里边找最小值,记录它的下标
            }
            //然后交换
            if(i!=min)//如果最小值不是它自己我们才进行交换了。
                swap(array,i,min);
        }

    }

3)Demo演示

public static void main(String[]args){
        int[]array = {45,12,65,89,66,99,32,564,78};
        for(int arr:array){
            System.out.print(arr+" ");
        }
        System.out.println();
        SelectSort.selectSort(array);
        for(int arr:array){
            System.out.print(arr+" ");
        }
    }

运行结果:
在这里插入图片描述

4)评价

首先我们肯定能够知道选择排序是不稳定的排序方法。它的比较次数与数据初始排序状态无关,第i趟比较的次数是 n-i移动的次数与初始的排列有关,如果本来就排序好了,移动0次,反之最大移动了3*(n-1)次(我们交换一次要三步,总次数n-1)。,总的比较次数N=(n-1)+(n-2)+...+1=n*(n-1)/2。
时间复杂度O(n^2),空间复杂度明显常数阶O(1)交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。但是因为每次都要选择最大的值交换,查找最大值浪费了好多时间。

3.4 看起来好烦的排序——堆排序(间接选择排序、完全二叉树)

1)原理
很口语的说法:你有一个神器,每次都能一下子找到当前数组序列里面的最小值所在(或者最大值所在,只能其一)那这个时候我们进行直接选择排序的时候我们就能节省了找最值的时间了。直接交换到当前序列第一个元素就行了。然后,在剩下的数组序列里边我们再用神器找到最小值所在,我们就交换,重复,直到排完数组的数。
那么这个神器是什么呢?那就是——,用数组实现的一个有特别属性完全二叉树戳我一下详细了解堆的结构
堆分为两种

  • 最大堆,双亲结点的值比每一个孩子结点的值都要根结点值最大
  • 最小堆,双亲结点的值比每一个孩子结点的值都要根结点值最小

注意:堆的根结点中存放的是最大或者最小元素,但是其他结点的排序顺序是未知的。例如,在一个最大堆中,最大的那一个元素总是位于根结点 的位置,但是最小的元素则未必是最后一个元素。唯一能够保证的是最小的元素是一个叶结点,但是不确定是哪一个。

数组实现的堆(完全二叉树)图示:
在这里插入图片描述
可以和层序遍历联想一下就能清楚里面的index的规律。

基于上面的一些介绍,我们可以知道算法的步骤应该是:

  • 1)把输入的数组建立堆的结构,可以神器般速度获取(根结点)最大值(最小值)的所在
  • 2)使用直接选择排序将当前的堆获取的最大值(最小值)与当前序列的末尾元素进行交换
  • 3)此时我们破坏了堆原有的属性,我们要把剩下的数组序列进行重新建立堆
  • 4)回弹第二步,直到所有的数都排好序

所以难点就在于怎么建立堆,重新建立堆!

2)代码实现

    public static void heapSort(int[]array){
        //1.对传入的数组进行建立堆,这里默认建立最小堆
        heapSort(array,true);
    }
    public static void heapSort(int[]array,boolean minheap){
        for(int i=array.length/2-1;i>=0;i--){
            //创建最小最大堆,默认最小堆,即minheap=true
            sift(array,i,array.length-1,minheap);

        }
        //排序,使用直接选择排序
        for(int j=array.length-1;j>0;j--){
            //现在的数组第一个就是根结点,最小值所在,进行交换,把它放到最右边
            swap(array,0,j);
            //重新建立堆
            sift(array,0,j-1,minheap);//将剩下的j-1个数,把它们调整为堆, 实质上是自上而下,自左向右进行调整的
           
    }
    //建立堆的方法

    /**
     * 私有方法,只允许被堆排序调用
     * @param array 要排序数组
     * @param parent 双亲结点
     * @param end 数组长度
     * @param minheap 是否建立最小堆
     */
    private  static void sift(int[]array,int parent,int end,boolean minheap){
        int child = 2*parent+1;//利用公式,创建child是parent的左孩子,+1则是右孩子
        int value = array[parent];//获取当前双亲结点值
        for(;child<end;child=child*2+1){//遍历

            //注意这里的child必须小于end,防止越界,建立最小堆,右孩子如果比左孩子小,我们就将现在的孩子换到右孩子
            //因为现在如果右孩子大于双亲,自然左孩子也大于双亲
            if(child<end&&(minheap?array[child]>array[child+1]:array[child]<array[child+1]))//比较左孩子与右孩子的大小
                child++;//右孩子如果比左孩子大,我们就将现在的孩子换到右孩子

            //判断是否符合最小堆的特性, 如果右孩子大于双亲,自然左孩子也大于双亲,符合
            if(minheap?value>array[child]:value<array[child]){
              
                swap(array,parent,child);////右孩子没有大于双亲,我们将其交换
                parent = child;//然后我们更新双亲结点和孩子结点
            }
            //如果不是,说明已经符合我们的要求了。
            else
                break;

        }
    }

3)Demo
在这里插入图片描述
4)评价
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和数组末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。

5)应用
不仅用于排序算法,还可以应用于频繁选择极值的问题,如优先队列,、Huffman、Prim、Kruskal、Dijkstra、Floyd等算法。

3.5 分治法的典型应用——归并排序(分治递归)

1)原理
非常棒的归并排序讲解,图解

归并排序(英语:Merge sort,或mergesort),是创建在归并操作上的一种有效的排序算法,效率为 O(nlog n)}(大O符号)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
原理描述:递归回弹的过程真爽!

图示
在这里插入图片描述

我们递归序列进行对半分(奇数则相差1),继续递归进行对半分,直到无法再分
:然后递归回弹!开始对这些子序列进行排序及合并操作,两个小小序列合并成小序列,两个小序列合并成大序列,然后就排序好了。

作为一种典型的分而治之思想的算法应用,归并排序的实现分为两种方法:

  • 自上而下的递归;
  • 自下而上的迭代;

2)代码实现
个人比较喜欢递归实现:

   public static void mergeSort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        mergeSort(arr,0,arr.length-1,temp);
    }

    /**
     *
     * @param arr 传入数组
     * @param left 当前子数组的起始下标
     * @param right 当前子数组的结束下标
     * @param temp 拷贝暂存数组
     */
    private static void mergeSort(int[] arr,int left,int right,int []temp){
        if(left<right){//这里是递归结束的条件,我们是对半分,那当left==right的时候肯定大家都是只有一个元素了。
            int mid = (left+right)/2;//对半分,比如总长度是10,left=0,right=9,mid=4确实是中间分了,0~4,5~9
            //当长度9,left=0,right=8,mid=4,0~4,5~8
            mergeSort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            mergeSort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){
        int i = left;//左序列起始下标
        int j = mid+1;//右序列起始下标
        int t = 0;//临时数组指针
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){//比较两个序列第一个元素谁小,谁小先拷贝谁到temp,然后对应子序列下标加1
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//将左边剩余元素填充进temp中——左序列有一些数总是比右边的大的数
            temp[t++] = arr[i++];
        }
        while(j<=right){//将右序列剩余元素填充进temp中——右序列有一些数总是比左边的大的数
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while(left <= right){//注意这里的条件是left——right
            arr[left++] = temp[t++];
        }
    }

当然也得学会迭代法实现:

public static void mergeSort(int[] arr) {
	int[] orderedArr = new int[arr.length];//建立一个数组用于临时拷贝
	for (int i = 2; i < arr.length * 2; i *= 2) {//逆向看,一个数组被拆分到最后有几块,对应子序列起始下标末尾下标的规律是什么
		for (int j = 0; j < (arr.length + i - 1) / i; j++) {
			int left = i * j;//规律所在
			int mid = left + i / 2 >= arr.length ? (arr.length - 1) : (left + i / 2);
			int right = i * (j + 1) - 1 >= arr.length ? (arr.length - 1) : (i * (j + 1) - 1);
			int start = left, l = left, m = mid;
			while (l < mid && m <= right) {
				if (arr[l] < arr[m]) {
					orderedArr[start++] = arr[l++];
				} else {
					orderedArr[start++] = arr[m++];
				}
			}
			while (l < mid)
				orderedArr[start++] = arr[l++];
			while (m <= right)
				orderedArr[start++] = arr[m++];
			System.arraycopy(orderedArr, left, arr, left, right - left + 1);//拷贝
		}
	}
}

3)Demo
在这里插入图片描述
4)评价

归并排序严格遵循从左到右或从右到左的顺序合并子数据序列, 它不会改变相同数据之间的相对顺序, 因此归并排序是一种稳定的排序算法.分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n,比较操作的次数介于 (nlog n)/2nlog n-n+1。 赋值操作的次数是2nlog n。归并算法的空间复杂度为:O(n)

3.6 跟打牌一样的排序——插入算法(每个数都找到它应该插入的位置)

1)原理
以第一个数为起始子序列,从第二个数开始,将这个数与与子序列比较找到自己应该插入的位置,找到了,就插入进去,起始子序列变为从第一个数到第二个数在这之前需要数组腾出空间给它插入)。完成一个数的插入,继续下一个数。直到最后一个数插入完毕。

简单得跟我们打牌整理牌一样。

2)代码实现

   //从右边第二个数字开始,在与前面的子序列进行比较,找到属于自己的位置进行插入
    public static void straghtInsertSort(int []array){
        //
        straghtInsertSort(array,true);//默认进行升序
    }
    public static void straghtInsertSort(int []array,boolean flag){
        if(flag){//选择是升序还是降序排序
            for(int i=1;i<array.length;i++){//从第二个数开始
                int temp = array[i],j;
                for( j=i-1;j>=0&&temp<array[j];j--)//array[i]是要插入的数
                    //即从j到i-1这些数都需要往后面移动一位,空出的位置给array[i]插入,
                    array[j+1] = array[j];//每比较一次我们就知道这个数需要往后面诺出一个位置了。
                array[j+1] = temp ;//注意这里要加1
                System.out.println("插入位置是"+i+",插入的数字是"+temp);
            }
        }

        else{//代码基本一致
            for(int i=1;i<array.length;i++){//从第二个数开始,默认第一个数是最小的
                int temp = array[i],j;
                for( j=i-1;j>=0&&temp>array[j];j--)//array[i]是要插入的数
                    //即从j到i-1这些数都需要往后面移动一位,空出的位置给array[i]插入,
                    array[j+1] = array[j];//每比较一次我们就知道这个数需要往后面诺出一个位置了。
                array[j+1] = temp ;//注意这里要加1
                System.out.println("插入位置是"+i+",插入的数字是"+temp);
            }
        }

    }

3)Demo
在这里插入图片描述
4)优化
我们在查找该数的插入位置是用顺序查找,但是因为与该数比较的序列它是有序的啊,那不就可以用二分查找的办法了?这就是——二分法插入排序
代码:

 public static void straghtInsertSort1(int []array){//优化
                for(int i=1;i<array.length;i++){//从第二个数开始,默认第一个数是最小的
                    int temp = array[i],start=0,end=i-1,mid=-1;
                    for( ;end>=start;)//array[i]是要插入的数
                    {
                        mid = start+(end-start)/2;
                        if(array[mid]>temp)
                            end=mid-1;
                        else //元素相同时,我们也插入后面,肯能破坏稳定性
                            start=mid+1;
                    }
                    //移动元素
                    for(int len=i-1;len>=start;len--){
                        array[len+1] = array[len];
                    }
                    array[start]=temp;
                    System.out.println("插入位置是"+start+",插入的数字是"+temp);


                }

            }

5)展示
原算法未优化时
在这里插入图片描述
使用二分法插入优化时:
在这里插入图片描述
。。数据比较少,没体现出来。。。。
6)评价
跟打牌一样的算法,算法是稳定的。时间复杂度平均时间O(n^2),空间复杂度O(1)

3.7 改进直接插入排序的排序——希尔排序(缩小增量排序,Δ牛笔)

1)原理
很简单,一张图就能看懂
在这里插入图片描述
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

该方法实质上是一种分组插入方法
比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。
一般的初次取序列的一半为增量,以后每次减半,直到增量为1。

2)代码实现

  public static void shellSort(int[]array){
        for(int delta=array.length/2;delta>0;delta=delta/=2){//控制增量的变化

            for(int i=delta;i<array.length;i++){//遍历每个组
                int temp = array[i],j;
                //组内排序
                for(j=i-delta;j>=0&&temp<=array[j];j-=delta){
                    array[j+delta]=array[j];
                }
                array[j+delta] = temp;
            }
        }
    }

3)Demo
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

???????????????????????????

5)评价
希尔排序是非稳定排序算法。不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率。希尔排序的时间的时间复杂度为O( n^1.5)),希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。但是比O( n^2)复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。 此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法. 本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 Good?

3.8 传说中时间复杂度为O(n)的排序——计数排序(非比较稳定排序)

1)原理
计数排序(Counting sort)是一种稳定的线性时间排序算法。该算法于1954年由 Harold H. Seward 提出。计数排序使用一个额外的数组用来计数。

简单巧妙的原理,假设有数组A[10]值为:1~10个数,现在我们统计出现的每一个数的个数,如出现5一次我们记一次,假设出现了3次,我们就把3存进一个数组计数器相应的位置这个数组计数器相应的下标都加上或者减去一个定值(delta)能够让我们知道存进来的是数组A中的哪个数(看作映射),而这个计数器下标里边的数代表该数出现的次数

这样我们就知道一个数出现的次数是多少了。我们利用了映射——找到最大最小值max,min,建立数组计数器count的长度max-min+1,那我们令delta为0+min那count[0]存放的是(0+delta)的值出现的个数,即min的个数!

排序过程:就是把计数器里的数一个一个地放回去。

所以就分两步:

  • 计数
  • 放回排序

图解
在这里插入图片描述

2)代码实现

   public static void countSort(int[]array){
        //找到最大最小值,建立基于这个长度(max-min+1)的一个数组容器来计数,我们可以建立一个下标与存储的数组的0值的映射
        int delta,min=array[0],max=array[0];
        //1.找最大最小值
        for(int i=1;i<array.length;i++){
            if(array[i]<min)
                min=array[i];//找最小值
            if(array[i]>max)
                max=array[i];//找最大值
            }
        //建立一个用于计数的数组,对应的下标映射实际数组的值
        delta = 0+min;//映射关系就是计数的数组下标与实际的值相差delta
        int []count_map = new int[max-min+1]; //count_map里的值都初始化为0;
        //开始计数
        for(int i=0;i<array.length;i++)
            //实现计数,array[i]-delta表示array[i]在count_map的数组下标映射,同一个下标则表示相同的数,该数大小为i+delta,++则实现计数
            count_map[array[i]-delta]++;
        //计算完数组以后,我们开始依次放回,默认升序
        int i=0;//用来指示计数数组的下标是否需要移动,当对应的count_map[i]==0,i++
        for(int j=0;j<array.length;){//填充,j用来控制array下标
            if(count_map[i]>0){
               array[j] =i+delta;//放回一个数
               j++;
               count_map[i]--;//计数器减1;
           }
           else i++;//放完了,就进行放下一个不重复的数
       }
    }

3)Demo
还是上次选择排序的那个数组,我这里使用了降序:

 int []array1 = {12,121,5,45,4,4,74,978,979,7,4,6,64,6,4554,979,44,55,1,215,46,22,6464,7977,797,797,79,9,11};
        long  t1 = System.currentTimeMillis();
        for(int i=0;i<100;i++)

            countSort(array1);
        //countSort(array1,false);
        System.out.println((System.currentTimeMillis()-t1)+"ms");

在这里插入图片描述

4)评价
当输入的元素是n 个0到k之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1)这使得计数排序对于数据范围很大的数组,需要大量时间和内存。值得一提它是稳定的排序。

最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k)

3.9 快快快的桶装数据排序——桶排序(利用了快排的特性和计数里的类似映射)

1)原理

桶排序是计数排序的升级版。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排

让我想一下怎么用口水话说:

  • 1)我们收到一个数组,我们就以某种规则指定了桶的个数n每个饭桶有它自己的饭桶号码,从0到n-1(嘻嘻)
  • 2) 现在我们开始将数组里的数放进桶里了,放进哪个编号的桶呢?规则就是找到最大值,最小值max,min,进而制定了每个饭桶它的“饭量范围”,如图,设置桶的数量为5个空桶,找到最大值110,最小值7,每个桶的饭量范围20.8=(110-7+1)/5 。对于0号桶,饭量[min,min+20.8X1)1号桶饭量就是[min+20.8X1,min+20.8X2)则4号饭桶的饭量就是[min+20.8X4,min+20.8X5),然后我们的饭桶就能愉快的“吃饭了”,把数放进自己对应的饭桶!
  • 在这里插入图片描述
  • 3)现在就是将每个饭桶内部的数进行排序,你可以使用递归桶排序,直到每个桶剩下一个数,也可以使用其他排序方法对桶内的数排序
  • 4)将内部排好序的桶按照饭量大小进行合并(也就是饭桶的桶号)

一个很棒的视频讲解——点我快速理解桶排序

2)代码实现
我自己写的使用二维数组实现的:(有点麻烦,因为二维数组在插入数的时候,如果同时进行排序,要移动很多数)这里设置桶数10。当然我们也可以使用函数映射生成桶的个数

 public static void bucketSort(int[] array)//data为待排序数组
    {
        int n = array.length, max = array[0], min = array[0];
        int[][] bask = new int[10][n];//定义10个桶来放数据,桶的索引为0到9,每个桶的数量最多存放n个数,
        //1、找最大值最小值制定桶的饭量
        for (int i = 1; i < array.length; i++) {
            if (min > array[i])
                min = array[i];
            if (max < array[i])
                max = array[i];
        }
        
        int delta = (max - min ) / 10+1;//注意这里要加1,不然num会越界
       
        //定义一个数组来记录对应的桶现在吃了多少个数
        int[] len_bask = new int[n];//默认值为0
        

        //2、开始将数放进桶里
        for (int i = 0; i < n; i++) {//遍历原数组
            int num = (array[i] - min) / delta;//每一个数对应的桶号
            // 放进对应桶号的对应位置
            bask[num][len_bask[num]] = array[i];
            //桶内数的个数加1
            len_bask[num]++;
        }

        //3、各个桶分别排序,这里使用计数排序
        for (int i = 0; i < 10; i++) {//遍历桶,进行排序
            //因为我们使用二维数组实现,当前每个桶存放的数值个数为len_bask[i]
            if(len_bask[i]!=0)//对非空的桶排序
                countSort(bask[i], len_bask[i]);
        }
        //4、合并桶喽
        int k=0;
        for(int i=0;i<10;i++){//遍历桶,进行合并
            for(int j=0;j<len_bask[i];j++){
                array[k++]=bask[i][j];
            }
        }
    }

3)Demo
还是上次的计数排序的原数组
在这里插入图片描述
把打印信息去掉以后,只用了1ms,而计数排序(3.8的计数排序用了9ms
在这里插入图片描述
4)评价
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)(当N=M时)即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
5)应用
1.海量数据

一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,要求对这500 万元素的数组进行排序。
分析:对500W数据排序,如果基于比较的先进排序,平均比较次数为O(5000000log5000000)≈1.112亿。但是我们发现,这些数据都有特殊的条件: 100=<score<=900。那么我们就可以考虑桶排序这样一个“投机取巧”的办法、让其在毫秒级别就完成500万排序。
方法:创建801(900-100)个桶。将每个考生的分数丢进f(score)=score-100的桶中。这个过程从头到尾遍历一遍数据只需要500W次。然后根据桶号大小依次将桶中数值输出,即可以得到一个有序的序列。而且可以很容易的得到100分有
**人,501分有***人。

2.大文件的数据排序

在一个文件中有10G个整数,乱序排列,要求找出中位数。内存限制为2G。只写出思路即可(内存限制为2G意思是可以使用2G空间来运行程序,而不考虑本机上其他软件内存占用情况。) 关于中位数:数据排序后,位置在最中间的数值。即将数据分成两部分,一部分大于该数值,一部分小于该数值。中位数的位置:当样本数为奇数时,中位数=(N+1)/2 ; 当样本数为偶数时,中位数为N/2与1+N/2的均值(那么10G个数的中位数,就第5G大的数与第5G+1大的数的均值了)。
分析:既然要找中位数,很简单就是排序的想法。那么基于字节的桶排序是一个可行的方法。
思想:将整型的每1byte作为一个关键字,也就是说一个整形可以拆成4个keys,而且最高位的keys越大,整数越大。如果高位keys相同,则比较次高位的keys。整个比较过程类似于字符串的字典序。

参考资料:百度百科桶排序的应用

3.10 每次都看位数值的排序——基数排序(利用了桶,基数对应进制)

1)原理
以LSD实现为例:(从最低位开始进桶的基数排序)
如下图,最低位数相同的都进入相应下标的桶,如第一次,最低位是3的全进了3号桶最低位是9的全进了9号桶。因为正整数的最低位只能是0~9,所以我们使用数组bask的长度为10,相应的下标为对应的位数的值,全部数组都进桶了完成一次对当前最低位的遍历,然后将桶按照编号合并(跟桶排序的最后一步相同),我们开始进行下一个位的进桶,比如十位是1的全进1号桶,我们这个时候不去理个位了。重复直到遍历完最大的数的最高位。最后得到的数组就排好序了。这就是10进制的LSD基数排序原理
在这里插入图片描述
实际例子:
假设原来有一串数值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
1)首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
2)接下来将这些桶子中的数值重新串接起来,成为以下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
3)接下来将这些桶子中的数值重新串接起来,成为以下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。

2)代码实现
这里演示全是正整数的排序:

    public static void radixSort(int[]array){
        int n=1;//表示从个位开始
        int max=array[0];//最大的数默认a【0】
        int [][]baske=new int[10][array.length];//因为位数只能是0~9,所以我们建立了10个桶,每个桶最多容纳array.length个数
        //我们又是用二维数组表示桶,则一样需要一个数组来记录该桶里面存了多少个数
        int []len_bask = new int[array.length];
        for(int i=0;i<array.length;i++){
            len_bask[i]=0;//初始化为0
            if(max<array[i])
                max = array[i];
        }
        int k=0;//用来控制array数组赋值下标
        //找到该数组里最大是数有几位
        while(max!=0){
            for (int value : array) {
                //取最低位的数值出来
                int lsd = (value / n) % 10;
                baske[lsd][len_bask[lsd]] = value;//位数相同的进同一个桶,
                len_bask[lsd]++; // 同时桶内数的个数加1
            }
            //现在将桶内的数取出来合并
            for(int i=0;i<10;i++){//外围控制桶的下标
                if(len_bask[i]!=0)//桶不空
                {
                    for(int j=0;j<len_bask[i];j++){//控制桶内
                        array[k]=baske[i][j];
                        k++;
                }
                 len_bask[i]=0;//遍历完该桶内的数,桶内的数就没有了。
                }
            }
            //继续进行下一个位
            k=0;
            n *=10;
            max/=10;//,同时我们让最大值位数降1,假设刚才是max=9,我们就只需要比较一个位数了。
        }
    }

3)Demo
还是上次的那个数组:
在这里插入图片描述
4)评价
标准的基数排序每次从小到大排序十进制下的一个位,并且只关心这一个位,也可以进行设计二进制的基数排序。
在这里插入图片描述
不知道细心的伙伴发现没有,上面的算法我们只能进行正整数的排序。那假设有负整数和正整数呢?我们可以加桶的个数,比如0~9的桶装正整数,10到19的桶装负整数;那如果不是整数呢?感谢IEEE 754的标准,它制定的浮点标准还带有一个特性:如果不考虑符号位(浮点数是按照类似于原码使用一个bit表示符号位s,s = 1表示负数),那么同符号浮点数的比较可以等价于无符号整数的比较,方法就是先做一次基数排序,然后再调整正负数部分顺序即可。所以现在已经可以实现浮点数的基数排序了!!!点我查看基数排序之浮点数排序

最佳情况:T(n) = O(n * k) 最差情况:T(n) = O(n * k) 平均情况:T(n) = O(n * k)

基数排序有两种方法:

  • MSD 从高位开始进行排序
  • LSD 从低位开始进行排序

5)基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶(根据位数)
  • 计数排序:每个桶只存储单一键值(下标映射实际的数)
  • 桶排序:每个桶存储一定范围的数值(根据一个函数映射得到一个桶容纳数的范围)

4、总结

借一张图来总结吧:
在这里插入图片描述
以上是一边学一边总结的,如果有错误,请大佬们给小弟指出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章