数据结构与算法----十大排序

在讲解各大排序算法前,我们先了解一下稳定排序,就是待排序的元素列中可能存在两个或两个以上关键字相等的记录。排序前的序列中Ri领先于Rj(即i<j).若在排序后的序列中Ri仍然领先于Rj,则称所用的方法是稳定的。比如int数组[1,1,1,6,4]中a[0],a[1],a[2]的值相等,在排序时不改变其序列,则称所用的方法是稳定的。

冒泡排序

这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
算法思想:
重复地访问过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。访问元素的步骤是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。

  1. 从当前元素起,向后依次比较每一对相邻元素,若逆序则交换 。
  2. 对所有元素均重复以上步骤,直至最后一个元素 。

以数组[5,3,9,-4,7]升序排列为例讲解

排序过程图解:
当i=0时,进行第一趟排序,j=0,此时arr[j]>arr[j+1],所以要进行交换,然后一直向后遍历。
在这里插入图片描述
当j=2时,此时arr[2]>arr[2+1],所以进行交换,然后继续遍历。
在这里插入图片描述
当j=3时,此时arr[3]>arr[4],所以进行交换。
在这里插入图片描述
当=4时,不符合循环条件,退出内循环。
i=1,进行第二趟排序,当j=1时,arr[1]>arr[2],所以交换,然后继续遍历。
在这里插入图片描述
如此反复执行,直至元素列有序.
在图解中可以看到,每次遍历完一趟,其数组的最大值会交换到当前所遍历到的元素的最后。第一趟数组的最大值会交换到数组末尾,那么对于下一趟,数组的最大值已知在最后一个元素就不需要再去比较,所以内循环比较的次数会谁趟数逐一递减,对于元素个数为N的数组,进行N-1趟排序,就可以将前(N-1)的元素交换到末尾有序排列,对于剩下的那个元素来说,已经是最小的,也无需排序了。
代码实现:

	public static void bubbleSort(int[] arr) {
		int len = arr.length;
		//外循环为排序趟数,len 个数进行len - 1趟
        for(int i =0 ; i<len -1 ; i++) { 
        	//内循环为每趟比较的次数,第i趟比较len - i次
            for(int j=0 ; j<len -1-i ; j++) { 
            	//前一个元素比后一个元素大才交换,是想让较大的元素在数组末尾,是非降序排列,反之则是非升序排列
                if(arr[j]>arr[j+1]) {
                    swap(arr,j,j+1);
            	}
            }    
        }
    }
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

时间复杂度:
若元素列的初始状态是正序的,一趟扫描即可完成排序。所以冒泡排序最好情况的时间复杂度为O(n);
若元素列是反序的,需要进行N-1趟排序。每趟排序要进行N-i(1≤i≤N-1)次比较,且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:所以冒泡排序最坏情况下时间复杂度为O(n²);

冒泡排序的稳定性:
冒泡排序就是把小的元素往前调或者往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法

选择排序

算法思想:
选择排序是将元素列分为有序序列和无序序列,初始时,有序序列为空,无序序列就是元素列,首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

以数组[5,3,9,-4,7]升序排列为例讲解

排序过程图解:
当i=0时,说明有序序列为空,需要遍历整个数组,得到最小值(或最大值)的索引,然后交换。
在这里插入图片描述
当i=1时,有序序列末尾为索引为1处,遍历无序序列得到无序序列的最小值索引为3,交换。
如此反复执行直到元素列有序。

代码实现:
对于元素个数为N的元素列,每一次交换都会将无序序列中最小值(或最大值)和有序序列的末尾交换,最多只要进行N-1次交换就可以使元素列有序,因为无序序列只剩下最后一个元素,自然是元素列的最大值(或最小值),也就不需要再次交换了。

	public static void selectionSort(int[] arr){
        for(int i = 0; i < arr.length - 1; i++){//交换次数
            //先假设每次循环时,最小数的索引为i
            int minIndex = i;
            //每一个元素都和剩下的未排序的元素比较
            for(int j = i + 1; j < arr.length; j++){
                if(arr[j] < arr[minIndex]){//寻找最小数
                    minIndex = j;//将最小数的索引保存
                }
            }
            //经过一轮循环,就可以找出第一个最小值的索引,然后把最小值放到i的位置
            if(i!=minIndex)//等于i说明索引为i处就是无序序列的最小值(或最大值),无需交换
            	swap(arr, i, minIndex);
        }
    }
    //交换
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

时间复杂度:
最好情况是,已经有序,交换0次,时间复杂度为O(n);最坏情况交换n-1次,逆序交换n/2次,时间复杂度为O(n²)。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

稳定性:
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法

插入排序

插入排序,一般也被称为直接插入排序。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数+1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。如果要排序的元素个数过多,再插入值时,会进行大量的元素后移,消耗较大,一般用与已经有一部分数据排列好,并且越多越好。

算法思想:
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序。

以数组[5,3,9,-4,7]升序排列为例讲解

排序过程图解:
初始时我们假设,第1个元素已经有序,然后需要我们将索引为1的元素插入到有序序列中。
在这里插入图片描述
然后将索引为2的元素插入到有序表中,因为前一元素(5)比插入元素小,直接进行下一轮插入。
反复执行直到待插入元素都已经插入。

代码实现:

    public static void sort(int[] arr){
        //将数组元素按升序排列
        int n=arr.length;
        //依次将元素列插入有序表
        for (int i=1;i<n;i++){
        	//比较和后移合并,如果前一个数比当前元素大,就和其交换(后移),直到前一个元素小于(降序就是大于)当前元素或已经将插入元素移到了最前面。
            for(int j=i;j>0&&(arr[j]<arr[j-1]);j--){
                int temp=arr[j];
                arr[j]=arr[j-1];
                arr[j-1]=temp;
            }
        }
    }

时间复杂度:
在插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较N- 1次,时间复杂度为O(n);
最坏的情况是待排序数组是逆序的,此时需要比较次数最多,总次数记为:1+2+3+…+N-1,所以,插入排序最坏情况下的时间复杂度为O(n²);

稳定性:
插入排序只要前一个元素比当前大(或小)时才交换,而相等的情况下是不需要进行交换的,举个例子,【1,2,3,2,4】,当插入第二个2时,与第一个2比较,两者相等不符合条件不发生交换,所以第二个2仍然在第一个2的后面,所以插入排序是稳定的排序算法

希尔排序

希尔排序是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。

算法思想:
希尔排序是把元素按下标的一定增量进行分组,对每组使用直接插入排序算法排序,随着增量减小,每组包含的元素越来越多,当增量减少为1时,整个元素列恰被分成一组。

以数组[8,9,1,7,2,3,5,4,6,0]升序排列为例讲解

排序过程图解:
数组初始状态:
在这里插入图片描述

array.length>1,初始增量gap=array.length/2=5,就是将整个数组分为{8,3},{9,5},{1,4},{7,6},{2,0},如图所示;
在这里插入图片描述

再对这5组,进行直接插入排序,结果如图所示:
在这里插入图片描述
gap>1,所以gap=5/2=2,于是将数组分为两组,{3,1,0,9,7},{5,6,8,4,2}如图所示;
在这里插入图片描述
再对以上两组进行直接插入排序,结果如图:
在这里插入图片描述
gap>1,所以gap=2/1=1;再进行依次直接插入排序即可得到有序数组{0,1,2,3,4,5,6,7,8,9}如下图所示:
在这里插入图片描述

代码实现:

	public void ShellSort(int[] array){
        int gap = array.length;//增量
        while (gap>1) {    
            gap /= 2;   //增量每次减半    
            for (int i = 0; i < gap; i++) {        
            	//这个循环里其实就是一个插入排序                       
                for (int j = i + gap; j < array.length; j += gap) {
                    int k = j - gap;            
                    while (k >= 0 && array[k] > array[k+gap]) {
                        int temp = array[k];
                        array[k] = array[k+gap];
                        array[k + gap] = temp;                
                        k -= gap;            
                    }                
                }    
            }    
        }
	}

时间复杂度:
最好情况下为O(n );最坏情况下为O(n log2 n)。

稳定性:
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的排序算法

归并排序

算法思想:

归并排序算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
在这里插入图片描述
归并操作:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。

重复步骤3直到某一指针超出序列尾。
将另一序列剩下的所有元素直接复制到合并序列尾。

以数组[8,9,1,7,2,3,5,4,6,0]升序排列为例讲解

排序过程图解:
我们先对数组进行 操作,将数组不断分成两组,直到不可再分,每次分组left–mid为一组,mid+1–right一组,(left +(right-left)/2,等价于(left+right)/2,只是为了防止left+right时溢出。)结果如图所示:
在这里插入图片描述
分好之后,我们再借助辅助数组temp进行归并操作:
这里我们选择其中一个序列进行讲解:
0比1小,先将0放入temp数组中,然后i不动,j和index后移一位;
在这里插入图片描述

1比3小,先将1放入temp数组中,然后j不动,i和index后移一位;
在这里插入图片描述

2比3小,先将2放入temp数组中,然后j不动,i和index后移一位;
在这里插入图片描述
然后因为3,4,5,6均比7小,将它们依次放入temp数组,然后将左边有序序列剩余的元素复制到temp数组中,结果如图所示。
在这里插入图片描述

代码实现:

	public static void sort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        sort(arr,0,arr.length-1,temp);
    }
    private static void sort(int[] arr,int left,int right,int []temp){
        if(left<right){
            int mid = (left+right)/2;
            sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
            sort(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[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){
            arr[left++] = temp[t++];
       

时间复杂度:
在待排序序列有序的最好的情况下时间复杂度为O(n),最坏情况下为O(nlogn)。

稳定性:

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个元素(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以归并排序是稳定的排序算法。

还有一种TimSort排序算法是对归并排序算法的改进算法,有兴趣的可以去了解一下。

快速排序

算法思想:

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分。
  2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

排序过程图解:
标红的元素作为比较的基准,小于基准数的放左边,大于等于基准数的放右边。
在这里插入图片描述

代码实现:

	public static void QuickSort(int[] arr,int left,int right){
        if(left>right) return ;
        int num = arr[left];//以arr[left]为基准
        int l = left;
        int r = right;
        while(l<r){
            //从后往前找小于基准数的
            while(l<r&&arr[r]>num) r--;
            //从前往后找大于基准数的
            while(l<r&&arr[l]<num) l++;
            //left---l区域和r--right区域没有重叠就交换
            if(l<r){
                int t = arr[l];
                arr[l] = arr[r];
                arr[r] = t;
            }
        }
        arr[l] = num;
        QuickSort(arr,left,l-1);
        QuickSort(arr,l+1,right);
    }

时间复杂度:
最好情况O(nlogn)最坏情况O(n²);
稳定性:
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。

堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父结点。
我们先来了解几个概念:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

如图所示就是大顶堆:
数组{50,45,40,20,25,35,30,10,15}对应着下图:
在这里插入图片描述

算法思想:

  1. 将初始待排序关键字序列(R1,R2…Rn)构建成大顶堆,此堆为初始的无序区,就是将每一个非叶子结点与这个结点及其子结点的三者之间最大(或最小)的结点进行交换,从最后一个非叶子结点开始(最后一个非叶子结点对应数组中的位置是 数组的长度/2 -1);
  2. 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,…Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  3. 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,…Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2…Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

以{10,45,30,20,15,40,30,50,25}升序排列为例讲解

排序过程图解:
第一步,先将待排序序列构建成一个大顶堆

  1. 由公式可得最后一个非叶子结点在数组中的索引= 9/2 -1 = 3,此时其左子结点left = index*2+1=7,大于当前是三者中最大的,所以交换left和index指向的值。
    在这里插入图片描述
    再到倒数第二个非叶子结点就是index = 2处,和其左右子结点三者间最大值为left=40,所以交换index和left指向的值。
    在这里插入图片描述
    再到倒数第三个非叶子结点就是index = 1处,和其左右子结点三者间最大值为left=50,所以交换index和left指向的值。
    在这里插入图片描述
    再到倒数第三个非叶子结点就是index = 0处,和其左右子结点三者间最大值为left=50,所以交换index和left指向的值。
    在这里插入图片描述
    第二步:交换
    一个大顶堆就构建完成,然后将堆顶元素与最后一个元素交换。
    得到结果如图所示:
    在这里插入图片描述
    第三步:因为交换导致违反了堆的性质所以需要调整堆,调整后如图所示:
    在这里插入图片描述

不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

代码实现:

	public static void heapSort(int[] arr){
        int len = arr.length;
        //构建堆
        for (int i = len/2-1; i >= 0 ; i--) {
            adjustHeap(arr,i,len);
        }
        for (int i = len-1; i > 0; i--) {
            swap(arr,0,i);//构建堆之后最值就在arr[0]与最后一个元素交换即可
            adjustHeap(arr,0,i);//交换之后堆可能无序,调整堆,
        }
    }

    private static void adjustHeap(int[] arr,int i,int len){
        int temp = arr[i],j;
        for(j = i*2+1;j<len;j=j*2+1){
        	//如果有右子树,并且右子树大于左子树,让j先指向子结点中最大的结点
            if(j+1<len&&arr[j]<arr[j+1]){
                j++;
            }
            //如果发现结点(左右子结点)大于根结点,则进行值的交换
            if(arr[j]>temp){
                arr[i] = arr[j];
                i=j;
                // 如果子结点更换了,那么,以子节点为根的子树会受到影响,所以,循环对子结点所在的树继续进行判断
            }else{
                break;
            }
        }
        arr[i] = temp;
    }

时间复杂度:

建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是log n,调用了n-1次,所以堆排序时间复杂度为O(n log n);
稳定性:

我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法

计数排序

计数排序是一个非基于比较的排序算法,它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序。
计数排序对输入的数据有附加的限制条件:

  1. 输入的线性表的元素属于有限偏序集S;
  2. 设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。
    在这两个条件下,计数排序的复杂性为O(n)。
    算法思想:

计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
排序过程图解:

在这里插入图片描述
对于稳定的计数排序,我们再往结果数组里存放元素时,要考虑基数排序的稳定行,这里采用累加和的方式,将计数数组累加(累加后的数组,存的值-1 得到的 是一组相同数的最后一个),我们只需要反向填充目标数组即可。
在这里插入图片描述

代码实现:

	public static int[] countSort(int[]arr){
		int len = arr.length;
        int[] result = new int[len];
        int max = arr[0],min = arr[0];
        for(int i:arr){
            if(i>max)max=i;
            if(i<min)min=i;
        }
        //这里k的大小是待排序的数组中,元素大小的极值差+1
        int k=max-min+1;
        //用于计数的辅助数组
        int[] count = new int[k];
        //计数
        for(int i=0;i<len;++i){
            count[arr[i]-min]+=1;//优化过的地方,减小了数组c的大小
        }
        //后面代码采用的是稳定的计数排序写法**************************
        //累加数组
        for(int i=1;i<k;++i){
            count[i]=count[i]+count[i-1];
        }
        //
        for(int i=len-1;i>=0;--i){
        	int t = arr[i]-min;//计算这个元素所在桶
            result[--count[t]]=arr[i];//计算arr[i]在桶中位置
        }
    	return result;
    }

时间复杂度:

O(n+k) (n是元素个数,k是"桶"的个数)。

稳定性:
采用累加和的方式,将计数数组累加(累加后的数组,存的值-1 得到的 是一组相同数的最后一个),然后反向填充目标数组,因为是反向填充,其相对顺序并不会发生改变,所以计数排序是稳定的排序算法

桶排序

算法思想:
桶排序是将集合中处于同一个值域的元素存入同一个桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成的多个桶,从值域上看是处于有序状态的。对每个桶中元素进行排序,则所有桶中元素构成的集合是已排序的。

桶排序与快速排序的联系:
快速排序是将集合拆分为两个值域,这里称为两个桶,再分别对两个桶进行排序,最终完成排序。桶排序则是将集合拆分为多个桶,对每个桶进行排序,完成排序过程。两者不同之处在于,快速排序是属于原地排序方式。桶排序则是提供了额外的空间,在额外空间上对桶进行排序,避免了构成桶过程的元素比较和交换操作,同时可以自主选择恰当的排序算法对桶进行排序。

桶排序对比基数排序上的改进:
桶排序是对计数排序的改进,计数排序需要的额外空间跨度从最小元素值到最大元素值,若待排序集合中元素不是依次递增的,则必然有空间浪费情况。桶排序则是弱化了这种浪费情况,将最小值到最大值之间的每一个位置申请空间,更新为最小值到最大值之间每一个固定区域申请空间,尽量减少了元素值大小不连续情况下的空间浪费情况。

桶排序过程中有两个关键点:

  1. 桶排序过程中怎样划分值域?
    元素值域的划分,也就是元素到桶的映射规则。映射规则需要根据待排序集合的元素分布特性进行选择,若规则设计的过于模糊、宽泛,则可能导致待排序集合中所有元素全部映射到一个桶上,则桶排序向比较性质排序算法演变。若映射规则设计的过于具体、严苛,则可能导致待排序集合中每一个元素值映射到一个桶上,则桶排序向计数排序方式演化。这里我们采用一个公式计算值域 gap=(max-min)/L+1;(max:待排序集合的最大元素,min:待排序集合的最小元素,L:待排序集合的元素个数)桶的个数就是(max-min)/gap+1;
  2. 对桶内元素排序时选择那种算法?
    排序算法的选择,从待排序集合中元素映射到各个桶上的过程,并不存在元素的比较和交换操作,在对各个桶中元素进行排序时,可以自主选择合适的排序算法,桶排序算法的复杂度和稳定性,都根据选择的排序算法不同而不同。

排序过程图解:
在这里插入图片描述

代码实现:

	public static void bucketSort(int[] arr){
        int len = arr.length;
        int max = arr[0],min = arr[0];
        //确定数组最大值和最小值
        for (int num : arr ) {
            if(num>max) max = num;
            if(num<min) min = num;
        }
        //计算值域
        int gap = (max-min)/len+1;
        //计算桶的个数
        int bucketNum = (max-min)/gap+1;
        //存放桶的集合
        List<List<Integer>> bucketSet = new ArrayList<>(bucketNum);
        //初始化桶集合
        for (int i = 0; i < bucketNum; i++) {
            bucketSet.add(new LinkedList<>());
        }
        //将待排序集合添加到其对应的桶
        for (int num : arr ) {
            int t = num/bucketNum;
            bucketSet.get(t).add(num);
        }
        int index = 0;
        List<Integer> temp ;
        for (int i = 0; i < bucketNum; i++) {
            temp = bucketSet.get(i);
            int size = temp.size();//桶中元素个数
            if(size>0){
                for (int j = 0; j < size; j++) {
                    //插入排序
                     arr[index] = temp.get(j);
                     for(int k = index;k>0&&arr[k-1]>arr[k];k--){
                         int t = arr[k];
                         arr[k] = arr[k-1];
                         arr[k-1] = t;
                     }
                     index++;
                }
            }
        }
    }

时间复杂度:

当数据可以均匀的分配到每一个桶中,就是最好的情况,时间复杂度为O(N)。
当输入的数据被分配到了同一个桶中,就是最坏的情况,时间复杂度为O(N²)。

稳定性:
桶排序的稳定性取决于桶内排序使用的算法。而本文中桶内元素排序使用直接插入排序,所以是稳定的排序算法。

基数排序

基数排序属于“分配式排序”,又称“桶子法”,顾名思义,它是透过键值的各个位的值,将要排序的元素分配至某些“桶”中,藉以达到排序的作用。基数排序是效率较高的稳定性排序法。是桶排序的拓展。

算法思想:

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

排序过程图解:

  1. 首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中;
  2. 将这些桶子中的数值重新串接起来,接着再进行一次分配,这次是根据十位数来分配;
  3. 再将这些桶子中的数值重新串接起来,接着再进行一次分配,这次是根据百位数来分配;
    如此往复执行,直到达到最大数的最高位都已经分配完,再串接起来就是有序了。
    在这里插入图片描述

代码实现:

	public static void sort(int[] number) {
		int max = number[0];
		for(int a:number) if(a>max) max = a ;
		int d = 0;//d表示最大的数有多少位
		while(max!=0){
			d++;
			max /= 10;
		}
        int k = 0;
        int n = 1;
        int m = 1; //控制键值排序依据在哪一位
        int[][] temp = new int[10][number.length]; //数组的第一维表示可能的余数0-9
        int[] order = new int[10]; //数组order[i]用来表示该位是i的数的个数
        while(m <= d){
            for(int i = 0; i < number.length; i++){
                int lsd = ((number[i] / n) % 10);//取出元素各位的值
                temp[lsd][order[lsd]] = number[i];//放入到对应的桶中
                order[lsd]++;
            }
            for(inti = 0; i < 10; i++){
                if(order[i] != 0){
                	//说明待排序集合中有第m位(低位到高位)为i的数
					for(int j = 0; j < order[i]; j++){
                        number[k] = temp[i][j];//将这些桶子中的元素重新串接起来
                        k++;
                    }
				}
				//清空桶内元素
                order[i] = 0;
            }
            n *= 10;
            k = 0;
            m++;
        }
    }

时间复杂度:

时间复杂度为O (n log®m),其中r为所采取的基数,而m为堆数。

稳定性:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法

十大排序算法比较:

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值