一、研究数组排序的意义:

数据结构中,排序算法各有用处,不同的排序方法有不同的时间复杂度与空间复杂度。为了能够依据不同情况,选用不同的排序方法解决不同的问题。

二、常见的数组排序方法:

以下研究,默认是对操作数组进行从小到大的排序。使用语言是Java。

1.选择排序法

选择排序法是将需要操作的数组分为已排序部分和未排序部分两部分。未排序的数组元素中,最小(或最大)的元素依次按照获得顺序放入已排序的元素中。

public static boolean sortByChoice(int[] arr) {
    if(arr == null || arr.length == 0) {
        return false;
   }
    for (int i = 0; i < a.length; i++) {
        for (int j = i + 1; j < a.length; j++) {
            int swap = arr[j];
            if (swap < arr[i]) {
                arr[j] = arr[i];
                arr[i] = swap;
           }
       }
   }
    return true;
}

在上述的排序方法中,我们是直接交换了数组中元素的值。那么请看下面一段代码:

public static boolean sortIndexByChoice(int[] a) {
    if(a == null || a.length == 0) {
        return false;
   }
    for (int i = 0; i < a.length; i++) {
        int mark = i;
        //记录需要交换的元素的下标
        for (int j = i + 1; j < a.length; j++) {
            if (a[mark] > a[j]) {
                mark = j;
           }
        //交换目标
        int swap = a[mark];
        a[mark] = a[j];
        a[j] = swap;
       }
   }
    return true;
}

上述方法中,通过定义指针变量mark指向(记录)数组中最小元素(的下标),直接交换指针指向的元素未排序部分首位的值进行交换。

选择排序法的时间复杂度为O(n^2);最多交换次数为N-1次。

2.冒泡排序法(起泡排序法)

冒泡排序法是在每次循环排序过程中,每次交换需要交换的相邻两个元素。冒泡排序法虽然理论上,时间复杂度和选择排序法相等,都是O(n^2),但是实际情况下,由于冒泡排序法较难避免重复比较,最坏情况下,程序执行比较的次数为n^n,消耗时间过长。

public static boolean bubbleSort(int[] a) {
 if(a == null || a.length == 0) {
        return false;
   }
 for (int i = 0; i < a.length-1;i++) {
  for (int j = 1; j < a.length - i; j++) {
   if (a[j-1] > a[j]) {
    int temp = a[j];
    a[j] = a[j-1];
    a[j-1] = temp;
   }
       }
 }
 return true;
}

将数组中相邻元素多次进行各个比较,它存在一个问题,即使数组已经有序,函数仍然在执行。因此,冒泡排序法可以尝试进行优化。请看下一段代码:

public static boolean bubbleSort1(int[] a) {
 if(a == null || a.length == 0) {
        return false;
   }
 for (int i = 0; i < a.length-1;i++) {
  boolean flag = false;
  for (int j = 1; j < a.length - i; j++) {
   if (a[j-1] > a[j]) {
    int temp = a[j];
    a[j] = a[j-1];
    a[j-1] = temp;
    if(!flag) {
     flag = true;
    }
   }
  }
  if(!flag) {
   break;
  }
 }
 return true;
}

相比起最开始的算法,加入了一个标识变量flag,用于标识是当前循环是否进行过交换。如果当前循环未进行交换,则终止外围循环。我们不难发现,每次循环后最大的数值会向后移动,这些元素被移动到数组末位之后是不需要比较的,而其实多轮循环中,无论第一种算法还是第二种算法,这些元素仍然产生比较的过程。是否可以再次进行优化?请看下面一段代码:

public static boolean bubbleSort2(int[] a) {
    if(a == null || a.length == 0) {
       return false;
   }
    
    //定义下标变量endPos代表下一次循环中,最后一个需要比较的元素的下标。
    int endPos = a.length - 1; 
    while(endPos > 0) {
        int flag = 1;
        for(int i = 1; j <= endPos; j++) {
            if (a[j-1] > a[j]) {
                int temp = a[j];
                a[j]= a[j-1];
                a[j-1] =temp;
                
                //由于该指针不断被重新赋值,将该指针指向最后一位参与交换的元素下标。
                flag = j;
           }
       }
        //调整endPos,使下一次循环只对flag指向的元素前面的元素进行比较与排序。flag后面的元素已拍好。
        endPos = flag - 1; 
   }
}

与bubbleSioer1相似的是,额外定义了一个指针变量flag,指向每次循环最后一次参与交换的数组元素。因此,每次循环只对未被排序的元素进行比较。

接下来,请看下面一段代码:

public static boolean bubbleBothway(int[] arr) {
    if(arr == null || arr.length == 0) {
        return false;
   }
 int high = arr.length - 1;
 int low = 0;
 while (low < high) {
  for(int i = low ;  i < high;i++) {
   if(arr[i] > arr[i+1]) {
    int temp = arr[i];
    arr[i] = arr[i+1];
    arr[i+1] = temp;
   }
  }
   high--;
   
   for(int j= high; j > low;  j--) {
    if(arr[j - 1] > arr [j]) {
          int swap = arr[j - 1];
          arr[j - 1] = arr[j];
          arr[j] = swap;
    }
   }
   low++;
 }
 return true;
}

这种算法是冒泡程序法的一种新的改进方法。相比于前面三种方法,它通过双向比较,同时,规避了重复比较的情况,效率相对前面更高。

3.插入排序法

插入排序法是将未排序部分的首位元素抽取出来,插入至已排序元素的合适位置。

public static boolean insertSort(int[] arr) {
    if(arr == null || arr.length == 0) {
        return false;
   }
 for(int i = 1; i < arr.length; i ++) {
  int temp = arr[i];
  int j = i - 1;
  while (temp < arr[j]) {
   arr[j + 1] = arr[j]; //后移前面的元素
   j--;
   if(j < 0) {  //需要插入在首位时
    break;
   }
  }
  arr[j + 1] = temp;  //插入元素
 }
 return true;
}

插入排序法理解起来相对简单,时间复杂度同为O(n^2)。

4.希尔排序法

希尔排序法是插入排序法的一种改进。希尔排序法的基本思想是:将数组分成若干个子序列,对于若干个子序列,进行插入排序,然后将这些序列进行插入排序。希尔排序法的核心问题就是在选择序列上。这些序列是采取一定的增量数据组成一个序列,随着排序过程,这个增量会减小。一般情况下,初始增量取数组长度的一半,然后每次再进行折半,直到增量为1一般情况下,初始增量取数组长度的一半,然后每次再进行折半,直到增量为1。请看下面一个例子:

假设说,下面有一个数组

int arr[10]= {10, 9, 7, 2, 5, 6, 8, 4, 19, 1};

假设此次增量取5,那么就有以下子序列

{a[0] = 10, a[5] = 6}
    {a[1] = 9, a[6] = 8}
 {a[2] = 7, a[7] = 4}
 {a[3] = 2, a[8] = 19}
 {a[4] = 5, a[9] = 1}

然后,将每个增量通过排序后,得到下面的数组:

{6, 8, 4, 2, 1, 10, 9, 7, 19, 5}

然后,将增量折半,就有新的子序列,再将子序列插入排序:

{6, 4, 1, 9, 19} ->{1, 4, 6, 9, 19}
    {8, 2, 10, 7, 5} ->{2, 5, 7, 8, 10}

因此,得到新数组

{1, 2, 4, 5, 6, 7, 9, 8, 19, 10}

最后,对数组直接进行一次插入排序

{1, 2, 4, 5, 6, 7, 8, 9, 10, 19}

 

使用java实现希尔排序法的源代码如下:

public static boolean shellSort(int[] arr) {
    if(arr == null || arr.length == 0) {
        return false;
   }
    int h = arr.length / 2;   //定义增量变量h
    while(h >= 1) {
     for (int i =h; i< arr.length; i++) {
     int j = i - h;   //依据增量,开始分组。
     int temp = arr[i];   
                
                /*子序列插入位置后的元素向后移动*/
     while(j>=0 && arr[j]>temp) {
      arr[j + h] = arr[j];  
      j -= h;       
     }
           
     arr[ j+h ] = temp;  //移动完成,插入元素值
    }
     h  /= 2;     //缩小增量
   }
    return true;
 }

希尔排序法相对于插入排序法,减少了数组中大量的元素移动的过程。希尔排序法思路和代码相对复杂。

5.快速排序法

快速排序法是最受到程序员欢迎的一种算法。它的思想方法是将一个容量较大的数组分成多个小数组,然后将各个小数组再次分成多个更小的数组,直到元素达到一定值时,开始比较各个小数组中的元素。

5.1递归法简介

在讨论快速排序法的问题之前,我们先说一下什么是函数的递归法:

先看一下下面的数学问题:



已知等差数列的递推公式a(n) = a(n-1) +2,其初始项a(1)=2,求其第18项a(18)。



的确,我们可以用求通用公式的方法求得结果。这里,我们不求结果,先讨论一下这个展开过程

这个数列求a(18)可以看做:

a(18)= a(18-1) + 2
     =(a((18-1)-1)+2)+2
     =((a(((18-1)-1)-1)+2)+2)+2
     ...

递推套用递推,直到找到基准值位置。其实这就是递归过程。

用java描述可以是:

public static int a(int x) {
    if (x = 1) {
        return 0;
   } else {
        return a(x - 1) + 2
   }
}

递归就是假定函数f(x),通过f(x)和f(ax + b)(a,b均为常数的关系,与当x=x0(x0为任意常数)时f(x)的值,求得给定任一x时f(x)的方法。f(x)=f(ax+b)+c为递归函数,f(x0)=f为基准。

5.2 快速排序法

现在,我们回到算法分析上。快速排序法的源代码如下:

public static boolean fastSort(int[] arr, int left ,int right) {
    if(arr == null || arr.length == 0||left<0||right>arr.length) {
    System.out.println("传参不合法!");
    return false;
   }
    if (left < right) {
     int s = arr[left];
     int i = left;
     int j = right +1;
     while(true) {
     //向右找大于s的元素下标
     //基本语法问题:由于++i已对i操作,这个while后面是一句空语句,用“;”隔开。
     while(i+1 < arr.length && arr[++i] < s) ;
     // 向左查找小于s的元素值的下标
     while(j-1> - 1 &&arr[--j]>s);
     if(i >= j) {
      // 如果左标i大于或等于右标j,退出死循环
      break;
     } else {
      // 交换i与j的位置
      int temp = arr[i];
      arr[i] = arr[j];
      arr[j]= temp;
     }
    } 
     arr[left] = arr[j];
     arr[j] = s;
     System.out.println(Arrays.toString(arr));
     //对左侧数组递归
     fastSort(arr, left, j-1);
    // 对右侧数组递归;
     fastSort(