java排序算法汇总

排序算法分类

这里写图片描述

各种排序算法比较

在这里插入图片描述
在这里插入图片描述
相关概念:

1、时间复杂度

 时间复杂度可以认为是对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
 常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n2)
 时间复杂度O(1):算法中语句执行次数为一个常数,则时间复杂度为O(1),

2、空间复杂度

空间复杂度是指算法在计算机内执行时所需存储空间的度量,它也是问题规模n的函数
空间复杂度O(1):当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1)
空间复杂度O(log2N):当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n)
                             ax=N,则x=logaN,
空间复杂度O(n):当一个算法的空间复杂度与n成线性比例关系时,可表示为0(n).

直接插入排序

基本思想

每步将一个待排序的记录,按其顺序码大小插入到前面已经排序的字序列的合适位置(从后向前找到合适位置后),直到全部插入排序完为止。
这里写图片描述

代码实现

public static void sort(int [] ages){
    for(int i = 1 ; i < ages.length ; i ++){
        for(int j = 0 ; j < i ; j ++){//与前面排好序的数字进行逐个比较,寻找合适的位置进行插入
            if(ages[j] > ages[i]){//全部移位
                int temp = ages[i];
                int tempIndex = i;
                while(tempIndex>j){
                    ages[tempIndex] = ages[tempIndex - 1];
                    tempIndex --;
                }
                ages[j] = temp;
            }
        }
    }
}

另一种写法:

public static void sort1(int[] nums) {
    for (int i = 1; i < nums.length; i++) {
        int moveindes = i;
        for (int j = i - 1; j >= 0; j--) {
            if (nums[moveindes] < nums[j]) {
                int temp = nums[moveindes];
                nums[moveindes] = nums[j];
                nums[j] = temp;
                moveindes--;
            } else {
                break;
            }
        }
    }
}

分析
直接插入排序是稳定的排序。
文件初态不同时,直接插入排序所耗费的时间有很大差异。若文件初态为正序,则每个待插入的记录只需要比较一次就能够找到合适的位置插入,故算法的时间复杂度为O(n),这是最好的情况。若初态为反序,则第i个待插入记录需要比较i+1次才能找到合适位置插入,故时间复杂度为O(n2),这是最坏的情况。
直接插入排序的平均时间复杂度为O(n2)。而且还要对数组中的数字进行逐个移位,所以性能比较差。

二分插入排序

基本思想
二分法插入排序的思想和直接插入一样,只是找合适的插入位置的方式不同,这里是按二分法找到合适的位置,可以减少比较的次数。

代码实现

/**
 * 二分查找算法, 找到返回对应的index,没找到返回比target大的第一个数的index
 * @param arrays
 * @param target
 * @return
 */
public static int query(int[] nums, int target, int begin, int end) {
    if (nums[end] < target) return -1;//全部比目标值小
    while (end >= begin) {
        int middle = (begin + end) / 2;
        if (nums[middle] == target) return middle;
        if (nums[middle] > target) {
            end = middle - 1;
        } else {
            begin = middle + 1;
        }
    }
    return begin;
}

/**
 * 直接插入排序
 * @param ages
 */
public static void sort2(int[] nums) {
    for (int i = 1; i < nums.length; i++) {
        int index = query(nums, nums[i], 0, i - 1);
        if (index == -1) continue;
        int temp = nums[i];
        for (int j = i; j > index; j--) {
            nums[j] = nums[j - 1];
        }
        nums[index] = temp;
    }
}

分析
这里只是对插入排序的一点点优化,平均时间复杂度还是O(n2)

希尔排序

基本思想
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。该方法实质上是一种分组插入方法。
这里写图片描述
代码实现

public static void shellSort(int [] a){
    int i, j, gap,n = a.length;

    for (gap = n / 2; gap > 0; gap /= 2) //步长
        for (i = 0; i < gap; i++){        //直接插入排序
            for (j = i + gap; j < n; j += gap)
                if (a[j] < a[j - gap]){
                    int temp = a[j];
                    int k = j - gap;
                    while (k >= 0 && a[k] > temp) {
                        a[k + gap] = a[k];
                        k -= gap;
                    }
                    a[k + gap] = temp;
                }
        }
}

//第二种写法:
public static void sort4(int[] a) {
    int i, j, step, temp;
    int x = 0;//统计交换次数
    for (step = a.length / 2; step >= 1; step /= 2) {
        for (i = step; i < a.length; i++) {
            temp = a[i];
            j = i - step;
            while (j >= 0 && temp < a[j]) {
                a[j + step] = a[j];
                j -= step;
                x++;
            }
            a[j + step] = temp;
        }
    }
    System.out.println(x);
}

分析
上面两种实现,交换次数相同。
我们知道一次插入排序是稳定的,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。

希尔排序的时间性能优于直接插入排序,原因如下:

(1)当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
  (2)当n值较小时,n和n2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。
  (3)在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
  因此,希尔排序在效率上较直接插人排序有较大的改进。
  希尔排序的平均时间复杂度为O(nlogn)。

简单选择排序

基本思想
在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和最后一个数比较为止。
这里写图片描述
代码实现

/**
     * 简单选择排序
     * @param ages
     */
    public static void sort(int [] ages){
        for(int i = 0 ; i < ages.length ; i ++){
            for(int j = i + 1 ; j < ages.length ; j ++){
                int minIndex = i;
                if(ages[j] < ages[minIndex]) minIndex = j;

                if(minIndex != i){
                    int temp = ages[i];
                    ages[i] = ages[minIndex];
                    ages[minIndex] = temp;
                }
            }
        }
    }

分析
简单选择排序是不稳定的排序。
时间复杂度:T(n)=O(n2)。

堆排序

基本概念

二叉树相关概念
二叉树相关概念请参考博主另一篇文章 java遍历二叉树

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

堆的定义下:具有n个元素的序列 (h1,h2,…,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,…,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二 叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。

堆排序的思想

利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。

其基本思想为(大顶堆):

  1. 将初始待排序关键字序列(R1,R2…Rn)构建成大顶堆,此堆为初始的无序区;
  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,则整个排序过程完成。

操作过程如下:

  1. 初始化堆:将R[1…n]构造为堆;

  2. 将当前无序区的堆顶元素R[1]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。

    因此对于堆排序,最重要的两个操作就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。

这里写图片描述

动画演示:http://upload.wikimedia.org/wikipedia/commons/4/4d/Heapsort-example.gif
这里写图片描述

代码实现

public static void heapSort(int[] array) {
    if (array == null || array.length == 0) return;

    buildMaxHeap(array);//先构造一次大顶堆

    //升序排序:逐个取最大值移动到末尾
    for (int i = array.length - 1; i > 0; i--) {
        switchNode(array, 0, i);
        maxHeap(array, 0, i);
    }
}

/**
 * 构建大顶堆
 * 
 * 注意:构建大顶堆时,这里是从数组的中间开始的,之后index--。原因是(arrayLength/2)位置后的节点都是叶子节点,没必要和子节点比较
 * 
 * @param array
 */
private static void buildMaxHeap(int[] array) {
    int half = array.length / 2;
    for (int i = half; i >= 0; i--) {
        maxHeap(array, i, array.length);
    }
}

private static void maxHeap(int[] array, int parentIndex, int arrayLength) {
    int left = parentIndex * 2 + 1;
    int right = parentIndex * 2 + 2;

    if (left < arrayLength && array[left] > array[parentIndex]) {
        switchNode(array, left, parentIndex);
        maxHeap(array, left, arrayLength);
    }
    if (right < arrayLength && array[right] > array[parentIndex]) {
        switchNode(array, right, parentIndex);
        maxHeap(array, right, arrayLength);
    }
}

private static void switchNode(int[] array, int a, int b) {
    int temp = array[a];
    array[a] = array[b];
    array[b] = temp;
}
@Test
public void test8() {
    int[] array = {4, 3, 5, 2, 1, 6, 0, 11, 66, 33, 22, 34, 66, 98, 12, 45, 23, 34, 32};
    heapSort(array);
    System.out.println(Arrays.toString(array));
}

输出结果:

[0, 1, 2, 3, 4, 5, 6, 11, 12, 22, 23, 32, 33, 34, 34, 45, 66, 66, 98]

分析

  • 时间复杂度

堆排序的时间复杂度,主要在初始化堆过程和每次选取最大数后重新建堆的过程;
初始化建堆过程时间:O(n)
推算过程:
首先要理解怎么计算这个堆化过程所消耗的时间,可以直接画图去理解;
假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;
那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要比较的次数,如果在最差的条件下,就是比较次数后还要交换;因为这个是常数,所以提出来后可以忽略;
S = 2^(k-2) * 1 + 2^(k-3)*2.....+2*(k-2)+2^(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
这个等式求解,我想高中已经会了:等式左右乘上2,然后和原来的等式相减,就变成了:
S = 2^(k - 1) + 2^(k - 2) + 2^(k - 3) ..... + 2 - (k-1)
除最后一项外,就是一个等比数列了,直接用求和公式:S = { a1[ 1- (q^n) ] } / (1-q)
S = 2^k -k -1;又因为k为完全二叉树的深度,所以(2^k) <= n < (2^k -1 ),总之可以认为:k = logn (实际计算得到应该是 log(n+1) < k <= logn );
综上所述得到:S = n - longn -1,所以时间复杂度为:O(n)

更改堆元素后重建堆时间:O(nlogn)
推算过程:
循环 n -1 次,每次都是从根节点往下循环查找,所以每一次时间是 logn,总时间:logn(n-1) = nlogn - logn ;

综上所述:堆排序的时间复杂度为:O(nlogn)

冒泡排序

冒泡排序是比较简单容易理解的,基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
这里写图片描述

public static void bubbleSort(int [] ages){
    for(int i = 0 ; i < ages.length ; i ++){
        for (int j = 0 ; j < ages.length - i - 1; j ++){
            if(ages[j] > ages[j + 1]){
                int temp = ages[j];
                ages[j] = ages[j + 1];
                ages[j + 1] = temp;
            }
        }
    }
}

分析
冒泡排序是稳定的排序。时间复杂度 n + (n-1) + (n-2) + … = (1+n)*n/2, 也就是 O(n2)。

快速排序

基本思想
先找一个基准值,一般使用第一个数据即可。第一次排序完成后,基准值左侧的都比基准值小,右侧的都比基准值大(升序情况下)。然后递归对两个分组进行排序即可。
基本步骤:先从右侧开始,逐个查找直至找到一个比基准值小的元素,交换。然后从左侧查询,直至找到一个比基准值大的元素,交换。循环该步骤即可。
代码实现

public static void sort7(int[] nums, int begin, int end) {
    if (begin > end) return;
    int base = nums[begin];
    int beginTemp = begin, endTemp = end;
    while (begin < end) {
        while (nums[end] > base && begin < end) {
            end--;
        }
        nums[begin] = nums[end];
        while (nums[begin] < base && begin < end) {
            begin++;
        }
        nums[end] = nums[begin];
    }
    nums[begin] = base;

    sort7(nums, begin + 1, endTemp);
    sort7(nums, beginTemp, end - 1);
}

分析
时间复杂度:O (nlogn)

归并排序

基本思想

在这里插入图片描述
在这里插入图片描述

代码实现
递归:

/**
     * 归并排序
     *
     * @param a
     * @param left
     * @param right
     */
    private static void guibingSort(int[] a, int left, int right) {
        if(left>=right)
            return;

        int mid = (left + right) / 2;
        //二路归并排序里面有两个Sort,多路归并排序里面写多个Sort就可以了
        guibingSort(a, left, mid);
        guibingSort(a, mid + 1, right);
        merge(a, left, mid, right);
    }

    private static void merge(int[] a, int left, int mid, int right) {

        int[] tmp = new int[a.length];
        int r1 = mid + 1;
        int tIndex = left;
        int cIndex=left;
        // 逐个归并
        while(left <=mid && r1 <= right) {
            if (a[left] <= a[r1])
                tmp[tIndex++] = a[left++];
            else
                tmp[tIndex++] = a[r1++];
        }
        // 将左边剩余的归并
        while (left <=mid) {
            tmp[tIndex++] = a[left++];
        }
        // 将右边剩余的归并
        while ( r1 <= right ) {
            tmp[tIndex++] = a[r1++];
        }




        System.out.println("第"+(++number)+"趟排序:\t");
        // TODO Auto-generated method stub
        //从临时数组拷贝到原数组
        while(cIndex<=right){
            a[cIndex]=tmp[cIndex];
            //输出中间归并排序结果
            System.out.print(a[cIndex]+"\t");
            cIndex++;
        }
        System.out.println();
    }
    static int number=0;
    @Test
    public void test9() {
        int[] nums = {14,12,15,13,11,16,4};
        guibingSort(nums, 0, nums.length-1);
        System.out.println(Arrays.toString(nums));
    }

分析

基数排序

基本思想

代码实现

分析

稳定排序和不稳定排序

概念描述

稳定排序就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。

说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些(个人感觉,没有证实)。

各个排序算法的稳定性分析

(1)冒泡排序

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

(2)选择排序

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

(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

(4)快速排序
快速排序有两个方向,左边的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] 交换的时刻。

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

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

(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比O(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

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

综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐崇拜234

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值