7种排序算法

Tags
#算法
首先抽象出一个排序基类:
package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午4:11
 */
public abstract class BaseSort {
    public abstract int[] sort(int[] array);

    public void swap(int[] arr, int a, int b) {
        int temp;
        temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
}
下面我会用7个实现类分别实现7种排序算法。

1.插入排序

数据结构 数组 最差时间复杂度 O(n^2) 最优时间复杂度 O(n) 平均时间复杂度 O(n^2) 最差空间复杂度 总共O(n) ,需要辅助空间O(1)

1.1.动图展示

notion image

1.2.算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
  1. 从第一个元素开始,该元素可以认为已经被排序
  1. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  1. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  1. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  1. 将新元素插入到该位置后
  1. 重复步骤2~5
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找插入排序。

1.3.示例代码

package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午5:04
 */
public class S1_InsertSort extends BaseSort {

  @Override
  public int[] sort(int[] array) {
      for (int i = 1; i < array.length; i++) {
          int temp = array[i];
          int j;
          for (j = i - 1; j >= 0 && temp < array[j]; j--) {
              array[j + 1] = array[j];
          }
          array[j + 1] = temp;
      }
      return array;
  }
}

2.选择排序

数据结构 数组 最差时间复杂度 О(n²) 最优时间复杂度 О(n²) 平均时间复杂度 О(n²) 最差空间复杂度 О(n) total, O(1) auxiliary

2.1.动图展示

notion image

2.2.算法描述

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

2.3.示例代码

package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午5:04
 */
public class S2_SelectSort extends BaseSort {

  @Override
  public int[] sort(int[] array) {
      for (int i = 0; i < array.length; i++) {
          int miniPost = i;
          for (int m = i + 1; m < array.length; m++) {
              if (array[m] < array[miniPost]) {
                  miniPost = m;
              }
          }

          if (array[i] > array[miniPost]) {
              int temp;
              temp = array[i];
              array[i] = array[miniPost];
              array[miniPost] = temp;
          }
      }
      return array;
  }
}

3.冒泡排序

数据结构 数组 最差时间复杂度 O(n^2) 最优时间复杂度 O(n) 平均时间复杂度 O(n^2) 最差空间复杂度 总共O(n),需要辅助空间O(1)

3.1.动图展示

notion image

3.2.算法描述

冒泡排序算法的运作如下:
  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  1. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  1. 针对所有的元素重复以上的步骤,除了最后一个。
  1. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

3.3.示例代码

package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午5:05
 */
public class S3_BubbleSort extends BaseSort {

  @Override
  public int[] sort(int[] array) {
      int temp;
      // 第一层循环:表明比较的次数, 比如 length 个元素,比较次数为 length-1 次(肯定不需和自己比)
      for (int i = 0; i < array.length - 1; i++) {
          for (int j = array.length - 1; j > i; j--) {
              if (array[j] < array[j - 1]) {
                  swap(array, j, j - 1);
              }
          }
      }
      return array;
  }
}

4.快速排序

数据结构 不定 最差时间复杂度 О(n^2) 最优时间复杂度 О(n log n) 平均时间复杂度 О(n log n) 最差空间复杂度 根据实现的方式不同而不同

4.1动图展示

notion image

4.2.算法描述

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。
步骤为:
  1. 从数列中挑出一个元素,称为"基准"(pivot),
  1. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。 3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。>递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

4.3.示例代码

package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午5:05
 */
public class S4_QuickSort extends BaseSort {

  @Override
  public int[] sort(int[] array) {
      return sortSub(array, 0, array.length - 1);
  }

  //排序子数组,采用分治思想,不断递归迭代,当每个子数组都排好了,源数组也就排好了
  private int[] sortSub(int[] arr, int low, int heigh) {
      if (low < heigh) {
          int division = partition(arr, low, heigh);
          sortSub(arr, low, division - 1);
          sortSub(arr, division + 1, heigh);
      }
      return arr;
  }

  // 分水岭,基位,左边的都比这个位置小,右边的都大
  private int partition(int[] arr, int low, int heigh) {
      int base = arr[low]; //用子表的第一个记录做枢轴(分水岭)记录
      while (low < heigh) { //从表的两端交替向中间扫描
          while (low < heigh && arr[heigh] >= base) {
              heigh--;
          }
          // base 赋值给 当前 heigh 位,base 挪到(互换)到了这里,heigh位右边的都比base大
          swap(arr, heigh, low);
          while (low < heigh && arr[low] <= base) {
              low++;
          }
          // 遇到左边比base值大的了,换位置
          swap(arr, heigh, low);
      }
      // now low = heigh;
      return low;
  }

}

5.合并排序

数据结构 数组 最差时间复杂度 O(n log n) 最优时间复杂度 O(n) 平均时间复杂度 O(n log n) 最差空间复杂度 O(n)

5.1.动图展示

notion image
notion image

5.2.算法描述

归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。
迭代法:
  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  1. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  1. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  1. 重复步骤3直到某一指针到达序列尾
  1. 将另一序列剩下的所有元素直接复制到合并序列尾
递归法:原理如下(假设序列共有n个元素):
  1. 将序列每相邻两个数字进行归并操作,形成floor(n/2)个序列,排序后每个序列包含两个元素
  1. 将上述序列再次归并,形成floor(n/4)个序列,每个序列包含四个元素3. 重复步骤2,直到所有元素排序完毕

5.3.示例代码

package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午5:05
 */
public class S5_MergeSort extends BaseSort {

  @Override
  public int[] sort(int[] array) {
      return sortSub(array, 0, array.length - 1);
  }

  private int[] sortSub(int[] nums, int low, int high) {
      int mid = (low + high) / 2;
      if (low < high) {
          // 左边
          sortSub(nums, low, mid);
          // 右边
          sortSub(nums, mid + 1, high);
          // 左右归并
          merge(nums, low, mid, high);
      }
      return nums;
  }

  private void merge(int[] nums, int low, int mid, int high) {
      int[] temp = new int[high - low + 1];
      int i = low;// 左指针
      int j = mid + 1;// 右指针
      int k = 0;
      // 把较小的数先移到新数组中
      while (i <= mid && j <= high) {
          if (nums[i] < nums[j]) {
              temp[k++] = nums[i++];
          } else {
              temp[k++] = nums[j++];
          }
      }
      // 把左边剩余的数移入数组
      while (i <= mid) {
          temp[k++] = nums[i++];
      }
      // 把右边边剩余的数移入数组
      while (j <= high) {
          temp[k++] = nums[j++];
      }
      // 把新数组中的数覆盖nums数组
      for (int k2 = 0; k2 < temp.length; k2++) {
          nums[k2 + low] = temp[k2];
      }
  }
}

6.希尔排序

数据结构 数组 最差时间复杂度 根据步长序列的不同而不同。已知最好的:O(n log^2 n) 最优时间复杂度 O(n) 平均时间复杂度 根据步长序列的不同而不同。 最差空间复杂度 O(n)

6.1.动图展示

以23, 10, 4, 1的步长序列进行希尔排序
notion image

6.2.算法描述

希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
假设有一个很小的数据在一个已按升序排好序的数组的末端。如果用复杂度为O(n2)的排序(冒泡排序或插入排序),可能会进行n次的比较和交换才能将该数据移至正确位置。而希尔排序会用较大的步长移动数据,所以小数据只需进行少数比较和交换即可到正确位置。
一个更好理解的希尔排序实现:将数组列在一个表中并对列排序(用插入排序)。重复这过程,不过每次用更长的列来进行。最后整个表就只有一列了。将数组转换至表是为了更好地理解这算法,算法本身仅仅对原数组进行排序(通过增加索引的步长,例如是用i += step_size而不是i++)。

6.3.示例代码

package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午5:05
 */
public class S6_ShellSort extends BaseSort {

  @Override
  public int[] sort(int[] array) {
      // 取增量
      int step = array.length / 2;
      while (step >= 1) {
          for (int i = step; i < array.length; i++) {
              int temp = array[i];
              int j = 0;
              // 跟插入排序的区别就在这里
              for (j = i - step; j >= 0 && temp < array[j]; j -= step) {
                  array[j + step] = array[j];
              }
              array[j + step] = temp;
          }
          step /= 2;
      }
      return array;
  }
}

7.堆排序

7.1.动图展示

notion image
堆排序算法的演示。首先,将元素进行重排,以匹配堆的条件。图中排序过程之前简单的绘出了堆树的结构。

7.2.算法描述

堆节点的访问:通常堆是通过一维数组来实现的。在数组起始位置为0的情形中:
  1. 父节点i的左子节点在位置(2*i+1);
  1. 父节点i的右子节点在位置(2*i+2);
  1. 子节点i的父节点在位置floor((i-1)/2);
堆的操作:在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义以下几种操作:
  1. 最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  1. 创建最大堆(Build_Max_Heap):将堆所有数据重新排序
  1. 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算

7.3.示例代码

package senior.algorithm.sort;

/**
 * 作者:余天然 on 16/7/8 下午5:05
 */
public class S7_HeapSort extends BaseSort {

  @Override
  public int[] sort(int[] array) {
      buildHeap(array);// 构建堆
      int n = array.length;
      int i = 0;
      for (i = n - 1; i >= 1; i--) {
          swap(array, 0, i);
          heapify(array, 0, i);
      }

      return array;
  }

  private void buildHeap(int[] array) {
      int n = array.length;// 数组中元素的个数
      for (int i = n / 2 - 1; i >= 0; i--)
          heapify(array, i, n);
  }

  private void heapify(int[] A, int idx, int max) {
      int left = 2 * idx + 1;// 左孩子的下标(如果存在的话)
      int right = 2 * idx + 2;// 左孩子的下标(如果存在的话)
      int largest = 0;// 寻找3个节点中最大值节点的下标
      if (left < max && A[left] > A[idx])
          largest = left;
      else
          largest = idx;
      if (right < max && A[right] > A[largest])
          largest = right;
      if (largest != idx) {
          swap(A, largest, idx);
          heapify(A, largest, max);
      }
  }

  // 建堆函数,认为【s,m】中只有 s
  // 对应的关键字未满足大顶堆定义,通过调整使【s,m】成为大顶堆
  public void heapAdjust(int[] array, int s, int m) {
      // 用0下标元素作为暂存单元
      array[0] = array[s];
      // 沿孩子较大的结点向下筛选
      for (int j = 2 * s; j <= m; j *= 2) {
          // 保证j为较大孩子结点的下标,j < m 保证 j+1 <= m ,不越界
          if (j < m && array[j] < array[j + 1]) {
              j++;
          }
          if (!(array[0] < array[j])) {
              break;
          }
          // 若S位较小,应将较大孩子上移
          array[s] = array[j];
          // 较大孩子的值变成S位的较小值,可能引起顶堆的不平衡,故对其所在的堆进行筛选
          s = j;
      }
      // 若S位较大,则值不变;否则,S位向下移动至2*s、4*s、。。。
      array[s] = array[0];
  }
}

参考目录

  1. 维基百科
  1. java常用的7大排序算法汇总

© fishyer 2022