排序方法的分类:

      插入类排序:直接插入排序、希尔排序

      交换类排序:冒泡排序、快速排序

      选择类排序:简单选择排序、堆排序

       归并类排序:归并排序。

总结:

 

一、稳定性:.

     稳定: 冒泡排序、插入排序、归并排序、基数排序、 折半插入排序

  不稳定:选择排序、快速排序、希尔排序、堆排序

 

二、平均时间复杂度

  O(n^2): 直接插入排序,简单选择排序,冒泡排序。

       O(nlogn): 快速排序,归并排序,希尔排序,堆排序。

  在数据规模较小时(9W内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为O(n^2)的算法基本上是相邻元素进行比较,基本上都是稳定的。

快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。

三、排序算法的选择

   1.数据规模较小

       1)待排序列基本序的情况下,可以选择直接插入排序;

       2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡

    2.数据规模不是很大

     1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出log(N)的额外空间。

        2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序 

    3.数据规模很大

          1)对稳定性有求,则可考虑归并排序。

           2)对稳定性没要求,宜用堆排序

     4.序列初始基本有序(正序),宜用直接插入,冒泡

java 排序与时间复杂度 java排序总结_父节点

注: 排序算法稳定性是指 :排序前后两个相等的数的相对顺序不变。

 

对每一个排序算法,需要了解以下几方面内容

------>

     1、每个算法的思想是什么?

     2、每个算法的稳定性怎样?时间复杂度是多少?

     3、在什么情况下,算法出现最好情况 or 最坏情况?

    4、每种算法的具体实现又是怎样的?

 

1.直接插入排序:

         算法思路:每次选择一个元素K插入到之前已排好序的部分A[1…i]中,插入过程中K依次由后向前与A[1…i]中的元素进行比较。若发现发现A[x]>=K,则将K插入到A[x]的后面,插入前需要移动元素。类似扑克牌洗牌过程。

        最好情况-----本身有序的情况下,共比较了n-1次,没有移动的记录,时间复杂度为O(n)。

        最坏情况-----逆序情况下:比较 n(n-1)/2次,移动次数为(n+2)(n-2)/2次。

         在插入排序中,K1是已排序部分中的元素,当K2和K1比较时,直接插到K1的后面,因此,插入排序是稳定的。

/**
     * 插入排序
     * @param array
     * @return
     */
    public static int[] insertionSort(int[] array) {
        if (array.length == 0)
            return array;
        int current;
        for (int i = 0; i < array.length - 1; i++) {
            current = array[i + 1];
            int preIndex = i;
            while (preIndex >= 0 && current < array[preIndex]) {
                array[preIndex + 1] = array[preIndex];
                preIndex--;
            }
            array[preIndex + 1] = current;
        }
        return array;
    }

2.希尔排序:

         算法思想:希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素一次性地朝最终位置前进一大步。先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的),分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

        最坏情况下:O(N*logN),最坏的情况下和平均情况下差不多。 

        平均情况下:O(N*logN)

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

/**
     * 希尔排序
     *
     * @param array
     * @return
     */
    public static int[] ShellSort(int[] array) {
        int len = array.length;
        int temp, gap = len / 2;
        while (gap > 0) {
            for (int i = gap; i < len; i++) {
                temp = array[i];
                int preIndex = i - gap;
                while (preIndex >= 0 && array[preIndex] > temp) {
                    array[preIndex + gap] = array[preIndex];
                    preIndex -= gap;
                }
                array[preIndex + gap] = temp;
            }
            gap /= 2;
        }
        return array;
    }

3.冒泡排序:

      算法思想:通过交换相邻的两个数变成小数在前大数在后,这样每次遍历后,最大的数就“沉”到最后面了。重复N次即可以使数组有序。通过设置标志位来记录此次遍历有无数据交换就可以判断是否要继续循环。

       最好情况-----正序有序,则只需要比较n次。故,为O(n) 

       最坏情况-----逆序有序,则需要比较(n-1)+(n-2)+……+1,故,为O(N*N)

       平均情况-----O(N*N)。

       排序过程中只交换相邻两个元素的位置。当两个数相等时,不交换两个数的位置的。所以,它们的相对位置并没有改变,冒泡排序算法是稳定的!

/**
     * 冒泡排序
     *
     * @param array
     * @return
     */
    public static int[] bubbleSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++)
            for (int j = 0; j < array.length - 1 - i; j++)
                if (array[j + 1] < array[j]) {
                    int temp = array[j + 1];
                    array[j + 1] = array[j];
                    array[j] = temp;
                }
        return array;
    }

4.快速排序:

         算法思想:从序列中挑出一个元素,作为"基准"(基准可以可以选择第一个数(实现一),或者中间数(实现二)), 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。

         最差情况 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)

         最好情况 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分 区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)

        平均时间复杂度 ---- O(nlogn)

        所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)

        快速排序是不稳定的排序算法,不稳定发生在基准元素与A[tail+1]交换的时刻。

/**
     * 快速排序方法
     * @param array
     * @param start
     * @param end
     * @return
     */

  public class Solution {
      public int partition(int [] input, int start, int end) {
        int flag = input[start];
        int first = start;
        start++;
        while(start<end){           
           while(input[end]>flag && start<=end)
               end--;
           while(input[start]<flag && start<=end)
               start++; 
            if(start<end){
                int temp =  input[end];
                input[end] = input[start];
                input[start] = temp;
            }
          }
       input[first] = input[end];
       input[end] =flag;
       return end;
    }
   
   public static int[] QuickSort(int[] array, int start, int end) {
        if (array.length < 1 || start < 0 || end >= array.length || start > end) return null;
        int smallIndex = partition(array, start, end);
        if (smallIndex > start)
            QuickSort(array, start, smallIndex - 1);
        if (smallIndex < end)
            QuickSort(array, smallIndex + 1, end);
        return array;
    }

  }

5.直接选择排序:

       算法思想:首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。

        最好情况下:交换0次,但是每次都要找到最小的元素,因此大约必须遍历N*N次,因此为O(N*N)。减少了交换次数!

        最坏情况下,平均情况下:O(N*N)

        由于每次都是选取未排序序列A中的最小元素x与A中的第一个元素交换,因此跨距离了,很可能破坏了元素间的相对位置,因此选择排序是不稳定的!

/**
     * 选择排序
     * @param array
     * @return
     */
    public static int[] selectionSort(int[] array) {
        if (array.length == 0)
            return array;
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) //找到最小的数
                    minIndex = j; //将最小数的索引保存
            }
            int temp = array[minIndex];
            array[minIndex] = array[i];
            array[i] = temp;
        }
        return array;
    }

6.堆排序:

        算法思路:首先要用数组创建一个最大堆,然后把第一个数和最后一个数交换,然后对第一个数到倒数第二个数调整顺序,创建新的最大堆,创建过程从最后一个父节点开始,如果有子节点大于父节点,那么把子节点和父节点交换,直到交换到叶子结点,然后对前一个父节点执行上述动作,直到对根节点执行完此操作 然后把新的第一个数和倒数第二个数交换,以此类推 。

       构建大堆顶的时间复杂度为O(n),第i次取堆顶记录重建堆需要O(logi)的时间,取n-1次,故重建堆的时间复杂度为O(nlogn)。无论最好,最坏,平均时间复杂度均为O(nlogn)

       堆排序需要不断地调整堆,因此它是一种不稳定的排序!

       从N个数据中找出最大的n个数,采用最小堆的方式,最顶端是最小值,再次遇到比它大的值,就可以入堆,入堆后重新调整堆,将小的值pass掉。这样我们就可以选出最大的前K个数据了。

java 排序与时间复杂度 java排序总结_时间复杂度_02

 实现代码如下:

//声明全局变量,用于记录数组array的长度;
static int len;
    /**
     * 堆排序算法
     *
     * @param array
     * @return
     */
    public static int[] HeapSort(int[] array) {
        len = array.length;
        if (len < 1) return array;
        //1.构建一个最大堆
        buildMaxHeap(array);
        //2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
        while (len > 0) {
            swap(array, 0, len - 1);
            len--;
            adjustHeap(array, 0);
        }
        return array;
    }
    /**
     * 建立最大堆
     *
     * @param array
     */
    public static void buildMaxHeap(int[] array) {
        //从最后一个非叶子节点开始向上构造最大堆
        for (int i = (len/2 - 1); i >= 0; i--) { //感谢 @让我发会呆 网友的提醒,此处应该为 i = (len/2 - 1) 
            adjustHeap(array, i);
        }
    }
    /**
     * 调整使之成为最大堆
     *
     * @param array
     * @param i
     */
    public static void adjustHeap(int[] array, int i) {
        int maxIndex = i;
        //如果有左子树,且左子树大于父节点,则将最大指针指向左子树
        if (i * 2 < len && array[i * 2] > array[maxIndex])
            maxIndex = i * 2;
        //如果有右子树,且右子树大于父节点,则将最大指针指向右子树
        if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
            maxIndex = i * 2 + 1;
        //如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
        if (maxIndex != i) {
            swap(array, maxIndex, i);
            adjustHeap(array, maxIndex);
        }
    }

 

7.归并排序:

        算法思路:多次将两个或两个以上的有序表合并成一个新的有序表。假设初始序列含有n个记录,则可以看成是n个能有序的子序列,每个子序列的长度为1,然后两两归并,再两两归并,…,如此重复,直至得到一个长度为n的有序序列为止。

        无论最好、最坏、平均来讲总的时间复杂度为O(nlogn)。

        归并排序最大的特色就是它是一种稳定的排序算法。归并过程中是不会改变元素的相对位置的。

/**
     * 归并排序
     *
     * @param array
     * @return
     */
    public static int[] MergeSort(int[] array) {
        if (array.length < 2) return array;
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(MergeSort(left), MergeSort(right));
    }
    /**
     * 归并排序——将两段排序好的数组结合成一个排序数组
     *
     * @param left
     * @param right
     * @return
     */
    public static int[] merge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        for (int index = 0, i = 0, j = 0; index < result.length; index++) {
            if (i >= left.length)
                result[index] = right[j++];
            else if (j >= right.length)
                result[index] = left[i++];
            else if (left[i] > right[j])
                result[index] = right[j++];
            else
                result[index] = left[i++];
        }
        return result;
    }

8.基数排序:

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

      基数排序的时间复杂度为O(d(n+r)),d为位数,r为基数。

      基数排序是稳定的排序算法

/**
     * 基数排序
     * @param array
     * @return
     */
    public static int[] RadixSort(int[] array) {
        if (array == null || array.length < 2)
            return array;
        // 1.先算出最大数的位数;
        int max = array[0];
        for (int i = 1; i < array.length; i++) {
            max = Math.max(max, array[i]);
        }
        int maxDigit = 0;
        while (max != 0) {
            max /= 10;
            maxDigit++;
        }
        int mod = 10, div = 1;
        ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
        for (int i = 0; i < 10; i++)
            bucketList.add(new ArrayList<Integer>());
        for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
            for (int j = 0; j < array.length; j++) {
                int num = (array[j] % mod) / div;
                bucketList.get(num).add(array[j]);
            }
            int index = 0;
            for (int j = 0; j < bucketList.size(); j++) {
                for (int k = 0; k < bucketList.get(j).size(); k++)
                    array[index++] = bucketList.get(j).get(k);
                bucketList.get(j).clear();
            }
        }
        return array;
    }

 

排序算法练习总结:

1.拓扑排序算法只能用于有向无环图中,在用邻接表表示图时,拓扑排序算法时间复杂度为O(n+e);

2.m个元素k路归并的归并趟数s=logk(m)

3.元素的移动次数与关键字的初始排列次序无关的是:基数排序、归并排序

4.元素的比较次数与初始序列无关是:选择排序、基数排序、折半插入排序

5.算法的时间复杂度与初始序列无关的是:直接选择排序 、堆排序、归并排序、基数排序【最好和最坏情况复杂度一样】

6.空间复杂度最高的排序方法:归并排序

7.若中序遍历平衡的二叉排序树,可得到排好序的关键码序列。

8.对list排序最快的方法是快速排序。

9.将N条长度均为M的有序链表进行合并,合并以后的链表也保持有序,时间复杂度O(N * M * logN)。---使用归并排序

10.排序时,若不采用计数排序的等空间换时间的方法,合并m个长度为n的已排序数组的时间复杂度最优为O(mn(logm))

11.归并排序最少比较N次,最多比较2N-1次。将两个各有N个元素的有序表归并成一个有序表,其最少的比较次数为N次。

12. 假设只有100Mb的内存,需要对1Gb的数据进行排序,最合适的算法是归并排序,【先将1G分为10个100M,分别加载10个100M数据到内存中,对其进行排序; 然后10文件每个文件选取10M加载到内存进行排序,依次进行。】

13. 精俭排序,即一对数字不进行两次和两次以上的比较,属于“精俭排序”的是:插入排序、归并排序

14.为实现快速排序算法,待排序序列宜采用的存储方式是顺序存储。

15.任何一个基于"比较"的内部排序的算法,若对6个元素进行排序,则在最坏情况下所需的比较次数至少为10次。【用基于比较的方法进行排序,在最坏情况下,能达到的最好时间复杂度为O(nlog2n),所以我们不管使用什么排序,低于[O(log2 6!)]次就不能囊括全部的6个元素序列排序。因此比较次数不能少于10次。】

16.内排序不要求数据一定要以顺序方式存储。

17.直接插入排序方法中比较次数最少的是逆序对最少的一组。

18.基数排序中,辅助数组的长度为max-min+1。

19.排序的方法有很多种,堆排序是基于选择排序的 一种方法,是完全二叉树结构的一个重要应用。

20.基数排序是一种基于统计的排序算法,遍历所有数据需要O(N)的时间复杂度,K为数据范围,遍历标记数组需要O(K),总的时间复杂度为O(N+K),空间复杂度为O(K)

21.枚举排序算法,通常也叫秩排序算法,基本思想是,对每一个要排序的元素,统计小于他的所有元素的个数,从而得到该元素在整个序列中的位置,时间复杂度为O(N^2)

22.一台机器对200个单词进行排序花了200秒(使用冒泡排序),那么花费800秒,大概可以对400个单词进行排序【 冒泡排序的时间复杂度为O(N^2),如果N^2=200,当M^2=800时,可以得到M/N=2,因此当N==200,M=400 】

23. 向一个有 127 个元素的顺序表中插入一个新元素并保持原来顺序不变,平均要移动的元素数是:63.5【最好的情况:在最后一个元素后面插入:移动0个

最坏的情况:在第一个元素前面插入:移动127个,平均移动元素个数:(127+0)/2=63.5】

24. 小根堆中最大的数一定是放在叶子节点上,堆本身是个完全二叉树,完全二叉树的叶子节点的位置大于[n/2]

25. 有些排序算法在每趟排序过程中,都会有一个元素被放置在最终位置上,希尔排序不会出现这样的情况。

26. 哈夫曼树定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

27.影响时间复杂度的主要因素为比较的次数。

28. 在某趟排序结束后不一定能选出一个元素放到其最终位置上的排序算法:希尔排序、直接插入排序

29.适合并行处理的排序算法:基数排序

30.具有 n 个结点的二叉排序树有多种,其中树高最小的二叉排序树是最佳的

31. 桶排序和基数排序均属于分配排序。分配排序的基本思想:排序过程无须比较关键字,而是通过用额外的空间来"分配"和"收集"来实现排序。在分配排序时,最高位优先分配法和最低位优先分配法难易程度一样。

32.对n个记录的线性表进行快速排序为减少算法的递归深度,每次分区后,先处理较短的部分。

33.对n个数字进行排序,两两不同的数字的个数为k,n远远大于k,而n的取值区间长度超过了内存的大小,时间复杂度最小可以是O(n)【先通过hash来获得这k个数,以及每个数对应的个数,然后对k个数进行排序。复杂度为O(N)】

34.选择排序的核心思想是从未排序数列中找到最小的数,和未排序数列中的首位交换位置,每扫描一遍数组,只需要一次交换。

35.在文件"局部有序"或文件长度较小的情况下,最佳内部排序的方法是:直接插入排序

36. 一个有n个节点的完全二叉树的最后一个非叶节点是节点[n/2],堆的初始化过程就从这个[n/2]节点开始。(15,9,7,8,20,-1,7,4),用排序的筛选方法降序排序建立的初始堆为(-1,4,7,8,20,15,7,9)

37.前序、中序、后序这三个两辆组合,必须要有中序才能唯一确定一棵二叉树, 前与后不能唯一确定。

38. AOE网(Activity On Edge Network)是边表示活动的网,AOE网是带权有向无环图。对AOV网进行拓扑排序的基本思想是:

(1)从AOV网中选择一个没有前驱的顶点输出它;

(2)从AOV网中删去该顶点,并且删去所有以该顶点为尾的弧;

(3)重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点。

AOE网是与排序拓扑有关, 而拓扑排序与关键路径有关

39. 影响外排序的时间因素主要是内存与外设交换信息的总次数

40. 已知表 A 中每个元素距其最终位置不远,则直接插入排序最省时间

41.二叉排序树删除一个结点后,仍是二叉排序树。在二叉排序树(二叉搜索树)中,最小值结点的左孩子一定为空指针。

42. 二叉排序树.它或者是一棵空树;或者是具有下列性质的二叉树:(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值; (2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值; (3)左、右子树也分别为二叉排序树(缺一不可)

43.对同一待排序序列分别进行折半插入排序和直接插入排序,两者之间可能的不同之处是:元素之间的比较次数。【折半插入排序,是对插入排序算法的一种改进,不按顺序依次寻找插入点,而是采用折半查找的方法来加快寻找插入点的速度。 】

44. 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行多路归并排序。

外排中使用置换选择排序的目的,是为了增加初始归并段的长度

外部排序的总时间 = 内部排序(产出初始归并段)所需时间 + 外存信息读取时间 + 内部归并所需的时间

45. DFS深度优先遍历从一个顶点出发:依次对访问过的顶点做标记,并寻找没有访问过的相邻顶点,找直接相连的点,依次找,找到尽头时,再往回溯。

无向图G=(V E),其中V={a,b,c,d,e,f},E={<a,b>,<a,e>,<a,c>,<b,e>,<c,f>,<f,d>,<e,d>}对该图进行深度优先排序,得到的顶点序列是{a,e,d,f,c,b}

46. 如表r有100000个元素,前99999个元素递增有序,则采用折半插入排序方法比较次数较少。

47. 基数排序是稳定的,但是应用于整数,不是实数!

48.每一次排序之后都能确定至少一个元素位置的排序方法包括:

1.选择排序:每次将最大的数放到最后。

2.冒泡排序:每一次排序最大的值位置确定。

3.快排:每一次排序pivot的位置确定。

4.堆排序:每一次排序时,都是将堆顶的元素和最后一个节点互换,然后调整堆,再将堆大小减1。所 以每一次排序堆顶元素确定。

 

不能至少确定一个元素的位置的方法包括:

1.插入排序:不到最后一步求的都是相对位置。

2.shell排序:对简单插入排序的改进。不到最后一步,是无法确定每个元素位置的。

3.归并排序:局部有序,并不能确定任一元素在全局的位置。

4.基数排序,计数排序:利用桶排序的思路,不是基于比较的排序,也无法在一次排序中确定某个元素的位置。因为每一次排序都是整体处理。