转载自:https://www.javazhiyin.com/13397.html 

数据结构  排序算法-LMLPHP 

稳定排序与不稳定排序 

直接插入排序 

直接插入排序介绍

直接插入排序图文说明

下面选取直接插入排序的一个中间过程对其进行说明。假设{20,30,40,10,60,50}中的前3个数已经排列过,是有序的了;接下来对10进行排列。示意图如下:

数据结构  排序算法-LMLPHP

 

图中将数列分为有序区和无序区。我们需要做的工作只有两个:(1)取出无序区中的第1个数,并找出它在有序区对应的位置。(2)将无序区的数据插入到有序区;若有必要的话,则对有序区中的相关数据进行移位。

直接插入排序的时间复杂度和稳定性

直接插入排序实现

Java实现

实现代码(InsertSort.java)

/**
* 直接插入排序:Java
*
*
*/

public class InsertSort {

   /*
    * 直接插入排序
    *
    * 参数说明:
    *     a -- 待排序的数组
    *     n -- 数组的长度
    */
   public static void insertSort(int[] a, int n) {
       int i, j, k;

       for (i = 1; i < n; i++) {

           //为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置
           for (j = i - 1; j >= 0; j--)
               if (a[j] < a[i])
                   break;

           //如找到了一个合适的位置
           if (j != i - 1) {
               //将比a[i]大的数据向后移
               int temp = a[i];
               for (k = i - 1; k > j; k--)
                   a[k + 1] = a[k];
               //将a[i]放到正确位置上
               a[k + 1] = temp;
           }
       }
   }

   public static void main(String[] args) {
       int i;
       int[] a = {20,40,30,10,60,50};

       System.out.printf("before sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");

       insertSort(a, a.length);

       System.out.printf("after  sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");
   }
}

输出结果:

before sort:20 40 30 10 60 50 
after  sort:10 20 30 40 50 60

 希尔排序

要点

希尔(Shell)排序又称为缩小增量排序,它是一种插入排序。它是直接插入排序算法的一种威力加强版。该方法因DL.Shell于1959年提出而得名。

希尔排序的基本思想是:

 

我们来通过演示图,更深入的理解一下这个过程。 

数据结构  排序算法-LMLPHP

 

在上面这幅图中:

初始时,有一个大小为 10 的无序序列。

在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组。接下来,按照直接插入排序的方法对每个组进行排序。

在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组。按照直接插入排序的方法对每个组进行排序。

在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组。按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。

需要注意一下的是,图中有两个相等数值的元素 5 和 5 。我们可以清楚的看到,在排序过程中,两个元素位置交换了。

所以,希尔排序是不稳定的算法。

 

核心代码

public void shellSort(int[] list) {

   int gap = list.length / 2;
 
   while (1 <= gap) {

       // 把距离为 gap 的元素编为一个组,扫描所有组
       for (int i = gap; i < list.length; i++) {
           int j = 0;
           int temp = list[i];

           // 对距离为 gap 的元素组进行排序
           for (j = i - gap; j >= 0 && temp < list[j]; j = j - gap) {
               list[j + gap] = list[j];
           }
           list[j + gap] = temp;
       }

       System.out.format("gap = %d:t", gap);
       printAll(list);
       gap = gap / 2; // 减小增量
   }
}

 

算法分析

希尔排序的算法性能

 

数据结构  排序算法-LMLPHP

 

时间复杂度

 

已知的最好步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,...),该序列的项来自这两个算式。这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

算法稳定性

由上文的希尔排序算法演示图即可知,希尔排序中相等数据可能会交换位置,所以希尔排序是不稳定的算法。

直接插入排序和希尔排序的比较

  • 直接插入排序是稳定的;而希尔排序是不稳定的。

  • 直接插入排序更适合于原始记录基本有序的集合。

  • 希尔排序的比较次数和移动次数都要比直接插入排序少,当N越大时,效果越明显。 

  • 在希尔排序中,增量序列gap的取法必须满足:最后一个步长必须是 1 。 

  • 直接插入排序也适用于链式存储结构;希尔排序不适用于链式结构。

 

完整参考代码

JAVA版本代码实现(范例代码中的初始序列和本文图示中的序列完全一致)。

package notes.javase.algorithm.sort;

public class ShellSort {
   public void shellSort(int[] list) {
       int gap = list.length / 2;

       while (1 <= gap) {
           // 把距离为 gap 的元素编为一个组,扫描所有组
           for (int i = gap; i < list.length; i++) {
               int j = 0;
               int temp = list[i];

               // 对距离为 gap 的元素组进行排序
               for (j = i - gap; j >= 0 && temp < list[j]; j = j - gap) {
                   list[j + gap] = list[j];
               }
               list[j + gap] = temp;
           }

           System.out.format("gap = %d:t", gap);
           printAll(list);
           gap = gap / 2; // 减小增量
       }
   }

   // 打印完整序列
   public void printAll(int[] list) {
       for (int value : list) {
           System.out.print(value + "t");
       }
       System.out.println();
   }

   public static void main(String[] args) {
       int[] array = {
               9, 1, 2, 5, 7, 4, 8, 6, 3, 5
       };

       // 调用希尔排序方法
       ShellSort shell = new ShellSort();
       System.out.print("排序前:tt");
       shell.printAll(array);
       shell.shellSort(array);
       System.out.print("排序后:tt");
       shell.printAll(array);
   }
}

 

运行结果

排序前:     9    1    2    5    7    4    8    6    3    5    
gap = 5:    4    1    2    3    5    9    8    6    5    7    
gap = 2:    2    1    4    3    5    6    5    7    8    9    
gap = 1:    1    2    3    4    5    5    6    7    8    9    
排序后:      1    2    3    4    5    5    6    7    8    9

冒泡排序 

冒泡排序介绍

冒泡排序(Bubble Sort),又被称为气泡排序或泡沫排序。

 

冒泡排序图文说明

 

 

 

下面以数列{20,40,30,10,60,50}为例,演示它的冒泡排序过程(如下图)。

数据结构  排序算法-LMLPHP

 

我们先分析第1趟排序

  • 当i=5,j=0时,a[0]<a[1]。此时,不做任何处理!

  • 当i=5,j=1时,a[1]>a[2]。此时,交换a[1]和a[2]的值;交换之后,a[1]=30,a[2]=40。

  • 当i=5,j=2时,a[2]>a[3]。此时,交换a[2]和a[3]的值;交换之后,a[2]=10,a[3]=40。

  • 当i=5,j=3时,a[3]<a[4]。此时,不做任何处理!

  • 当i=5,j=4时,a[4]>a[5]。此时,交换a[4]和a[5]的值;交换之后,a[4]=50,a[3]=60。

 

于是,第1趟排序完之后,数列{20,40,30,10,60,50}变成了{20,30,10,40,50,60}。此时,数列末尾的值最大。

 

根据这种方法:

  • 第2趟排序完之后,数列中a[5...6]是有序的。

  • 第3趟排序完之后,数列中a[4...6]是有序的。

  • 第4趟排序完之后,数列中a[3...6]是有序的。

  • 第5趟排序完之后,数列中a[1...6]是有序的。

 

第5趟排序之后,整个数列也就是有序的了。

冒泡排序的时间复杂度和稳定性

算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

冒泡排序实现

冒泡排序Java实现

实现代码(BubbleSort.java)

/**
* 冒泡排序:Java
*
*
*/

public class BubbleSort {

   /*
    * 冒泡排序
    *
    * 参数说明:
    *     a -- 待排序的数组
    *     n -- 数组的长度
    */
   public static void bubbleSort1(int[] a, int n) {
       int i,j;

       for (i=n-1; i>0; i--) {
           // 将a[0...i]中最大的数据放在末尾
           for (j=0; j<i; j++) {

               if (a[j] > a[j+1]) {
                   // 交换a[j]和a[j+1]
                   int tmp = a[j];
                   a[j] = a[j+1];
                   a[j+1] = tmp;
               }
           }
       }
   }

   /*
    * 冒泡排序(改进版)
    *
    * 参数说明:
    *     a -- 待排序的数组
    *     n -- 数组的长度
    */
   public static void bubbleSort2(int[] a, int n) {
       int i,j;
       int flag;                 // 标记

       for (i=n-1; i>0; i--) {

           flag = 0;            // 初始化标记为0
           // 将a[0...i]中最大的数据放在末尾
           for (j=0; j<i; j++) {
               if (a[j] > a[j+1]) {
                   // 交换a[j]和a[j+1]
                   int tmp = a[j];
                   a[j] = a[j+1];
                   a[j+1] = tmp;

                   flag = 1;    // 若发生交换,则设标记为1
               }
           }

           if (flag==0)
               break;            // 若没发生交换,则说明数列已有序。
       }
   }

   public static void main(String[] args) {
       int i;
       int[] a = {20,40,30,10,60,50};

       System.out.printf("before sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");

       bubbleSort1(a, a.length);
       //bubbleSort2(a, a.length);

       System.out.printf("after  sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");
   }
}

 

上面3种实现的原理和输出结果都是一样的。下面是它们的输出结果:

before sort:20 40 30 10 60 50 
after  sort:10 20 30 40 50 60

快速排序 

快速排序介绍

快速排序流程:

  1. 从数列中挑出一个基准值。

  2. 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。

  3. 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。

 

快速排序图文说明

下面以数列a={30,40,60,10,20,50}为例,演示它的快速排序过程(如下图)。

数据结构  排序算法-LMLPHP

 

上图只是给出了第1趟快速排序的流程。在第1趟中,设置x=a[i],即x=30。

  1. 从"右 --> 左"查找小于x的数:找到满足条件的数a[j]=20,此时j=4;然后将a[j]赋值a[i],此时i=0;接着从左往右遍历。

  2. 从"左 --> 右"查找大于x的数:找到满足条件的数a[i]=40,此时i=1;然后将a[i]赋值a[j],此时j=4;接着从右往左遍历。

  3. 从"右 --> 左"查找小于x的数:找到满足条件的数a[j]=10,此时j=3;然后将a[j]赋值a[i],此时i=1;接着从左往右遍历。

  4. 从"左 --> 右"查找大于x的数:找到满足条件的数a[i]=60,此时i=2;然后将a[i]赋值a[j],此时j=3;接着从右往左遍历。

  5. 从"右 --> 左"查找小于x的数:没有找到满足条件的数。当i>=j时,停止查找;然后将x赋值给a[i]。此趟遍历结束!

按照同样的方法,对子数列进行递归遍历。最后得到有序数组!

快速排序的时间复杂度和稳定性

快速排序稳定性

快速排序是不稳定的算法,它不满足稳定算法的定义。

算法稳定性 -- 假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!

快速排序时间复杂度

快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。

这句话很好理解:假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),需要遍历多少次呢?至少lg(N+1)次,最多N次。

(01) 为什么最少是lg(N+1)次?快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。因此,快速排序的遍历次数最少是lg(N+1)次。

(02) 为什么最多是N次?这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是N。因此,快读排序的遍历次数最多是N次。

 

快速排序实现

快速排序Java实现

实现代码(QuickSort.java)

/**
* 快速排序:Java
*
*/

public class QuickSort {

   /*
    * 快速排序
    *
    * 参数说明:
    *     a -- 待排序的数组
    *     l -- 数组的左边界(例如,从起始位置开始排序,则l=0)
    *     r -- 数组的右边界(例如,排序截至到数组末尾,则r=a.length-1)
    */
   public static void quickSort(int[] a, int l, int r) {

       if (l < r) {
           int i,j,x;

           i = l;
           j = r;
           x = a[i];
           while (i < j) {
               while(i < j && a[j] > x)
                   j--; // 从右向左找第一个小于x的数
               if(i < j)
                   a[i++] = a[j];
               while(i < j && a[i] < x)
                   i++; // 从左向右找第一个大于x的数
               if(i < j)
                   a[j--] = a[i];
           }
           a[i] = x;
           quickSort(a, l, i-1); /* 递归调用 */
           quickSort(a, i+1, r); /* 递归调用 */
       }
   }

   public static void main(String[] args) {
       int i;
       int a[] = {30,40,60,10,20,50};

       System.out.printf("before sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");

       quickSort(a, 0, a.length-1);

       System.out.printf("after  sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");
   }
}

输出结果:

before sort:30 40 60 10 20 50
after  sort:10 20 30 40 50 60

 简单选择排序

选择排序介绍

 

选择排序图文说明

下面以数列{20,40,30,10,60,50}为例,演示它的选择排序过程(如下图)。

数据结构  排序算法-LMLPHP

 

排序流程

  • 第1趟:i=0。找出a[1...5]中的最小值a[3]=10,然后将a[0]和a[3]互换。 数列变化:20,40,30,10,60,50 -- > 10,40,30,20,60,50

  • 第2趟:i=1。找出a[2...5]中的最小值a[3]=20,然后将a[1]和a[3]互换。 数列变化:10,40,30,20,60,50 -- > 10,20,30,40,60,50

  • 第3趟:i=2。找出a[3...5]中的最小值,由于该最小值大于a[2],该趟不做任何处理。 

  • 第4趟:i=3。找出a[4...5]中的最小值,由于该最小值大于a[3],该趟不做任何处理。 

  • 第5趟:i=4。交换a[4]和a[5]的数据。 数列变化:10,20,30,40,60,50 -- > 10,20,30,40,50,60

 

选择排序的时间复杂度和稳定性

 

选择排序实现

选择排序Java实现

实现代码(SelectSort.java)

/**
* 选择排序:Java
*
*
*/

public class SelectSort {

   /*
    * 选择排序
    *
    * 参数说明:
    *     a -- 待排序的数组
    *     n -- 数组的长度
    */
   public static void selectSort(int[] a, int n) {
       int i;        // 有序区的末尾位置
       int j;        // 无序区的起始位置
       int min;    // 无序区中最小元素位置

       for(i=0; i<n; i++) {
           min=i;

           // 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。
           for(j=i+1; j<n; j++) {
               if(a[j] < a[min])
                   min=j;
           }

           // 若min!=i,则交换 a[i] 和 a[min]。
           // 交换之后,保证了a[0] ... a[i] 之间的元素是有序的。
           if(min != i) {
               int tmp = a[i];
               a[i] = a[min];
               a[min] = tmp;
           }
       }
   }

   public static void main(String[] args) {
       int i;
       int[] a = {20,40,30,10,60,50};

       System.out.printf("before sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");

       selectSort(a, a.length);

       System.out.printf("after  sort:");
       for (i=0; i<a.length; i++)
           System.out.printf("%d ", a[i]);
       System.out.printf("n");
   }
}

输出结果:

before sort:20 40 30 10 60 50 
after  sort:10 20 30 40 50 60

堆排序

堆排序

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

数据结构  排序算法-LMLPHP

 

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子:

数据结构  排序算法-LMLPHP

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

ok,了解了这些定义。接下来,我们来看看堆排序的基本思想及基本步骤:

堆排序基本思想及步骤

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

1.假设给定无序序列结构如下

数据结构  排序算法-LMLPHP

 

2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

数据结构  排序算法-LMLPHP

 

3.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

数据结构  排序算法-LMLPHP

 

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

数据结构  排序算法-LMLPHP

此时,我们就将一个无需序列构造成了一个大顶堆。

 

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

 

a.将堆顶元素9和末尾元素4进行交换

数据结构  排序算法-LMLPHP

 

b.重新调整结构,使其继续满足堆定义

数据结构  排序算法-LMLPHP

 

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

数据结构  排序算法-LMLPHP

 

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

数据结构  排序算法-LMLPHP

再简单总结下堆排序的基本思路:

  1. 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

 

代码实现



import java.util.Arrays;

/**
*
*/
public class HeapSort {
   public static void main(String []args){
       int []arr = {9,8,7,6,5,4,3,2,1};
       sort(arr);
       System.out.println(Arrays.toString(arr));
   }
   public static void sort(int []arr){
       //1.构建大顶堆
       for(int i=arr.length/2-1;i>=0;i--){
           //从第一个非叶子结点从下至上,从右至左调整结构
           adjustHeap(arr,i,arr.length);
       }
       //2.调整堆结构+交换堆顶元素与末尾元素
       for(int j=arr.length-1;j>0;j--){
           swap(arr,0,j);//将堆顶元素与末尾元素进行交换
           adjustHeap(arr,0,j);//重新对堆进行调整
       }

   }

   /**
    * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
    * @param arr
    * @param i
    * @param length
    */
   public static void adjustHeap(int []arr,int i,int length){
       int temp = arr[i];//先取出当前元素i
       for(int k=i*2+1;k<length;k=k*2+1){
       //从i结点的左子结点开始,也就是2i+1处开始
           if(k+1<length && arr[k]<arr[k+1]){
           //如果左子结点小于右子结点,k指向右子结点
               k++;
           }
           if(arr[k] >temp){
           //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
               arr[i] = arr[k];
               i = k;
           }else{
               break;
           }
       }
       arr[i] = temp;//将temp值放到最终的位置
   }

   /**
    * 交换元素
    * @param arr
    * @param a
    * @param b
    */
   public static void swap(int []arr,int a ,int b){
       int temp=arr[a];
       arr[a] = arr[b];
       arr[b] = temp;
   }
}

结果

[1, 2, 3, 4, 5, 6, 7, 8, 9]


 

归并排序

基本思想

分而治之

数据结构  排序算法-LMLPHP

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。

合并相邻有序子序列

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

数据结构  排序算法-LMLPHP

数据结构  排序算法-LMLPHP

 

代码实现

package sortdemo;

import java.util.Arrays;

/**
*
*/
public class MergeSort {
  public static void main(String []args){
      int []arr = {9,8,7,6,5,4,3,2,1};
      sort(arr);
      System.out.println(Arrays.toString(arr));
  }
  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++];
      }
  }
}

执行结果

[1, 2, 3, 4, 5, 6, 7, 8, 9]

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

10-07 14:04