排序方法分类
-
**比较类排序:**通过比较来决定元素间的相对次序 时间复杂度不能突破O(nlogn),因此又被称为非线性时间比较类排序;
- 交换排序
- 冒泡排序
- 快速排序
- 插入排序
- (简单)插入排序
- 希尔排序
- 选择排序
- (简单)选择排序
- 堆排序
- 归并排序
- 交换排序
-
**非比较类排序:**不通过比较来决定元素的相对次序,时间复杂度可以突破到线性,又被称为线性时间非比较类排序。
- 计数排序
- 桶排序
冒泡排序 O(n^2)
不断比较相邻的两个元素,并交换位置,一次遍历确定一个最值放在最后。
// 冒泡排序: 稳定 时间复杂度O(n^2)
public class BubbleSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,0,-2,14};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void bubbleSort(int[] arr){
int len = arr.length;
for(int i = 1; i < len; i++){
for(int j = 0; j < len - i; j++){
if(arr[j] > arr[j+1]){
swap(arr, j, j+1);
}
}
}
}
private static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
快速排序 O(nlogn) ★
整体流程:
- 先从数列中取出一个数作为基准数pivot(一般取第一个数,但是可能会出现时间复杂度最差的情况)
- 分区,把比pivot大的数都放在它的右边,比pivot小的数都放在它的左边
- 设置两个遍历i、j,排序开始时取i=0,j=N-1
- 以第一个元素为pivot,即pivot=arr[0]=arr[i]
- 由后向前搜索(j–),找到第一个小于pivot的数arr[j],将arr[j]和arr[i]交换
- 由前向后搜索(i++),找到第一个大于pivot的数arr[i],将arr[i]和arr[j]交换
- 重复前两步,直到i==j,此时pivot左边的数都时比它小的(无序),右边的数都是比它大的(无序)
- 对左右两个区间重复前两步,直到各个区间都只有一个数,此时排序就完成了
// 快速排序: 不稳定,时间复杂度为O(nlogn),当pivot为最值的时候,时间复杂度最差,为O(n^2)
public class QuickSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,0,-2,14};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void quickSort(int[] arr, int low, int high){
if(low < high){
int i = low;
int j = high;
int pivot = arr[i];
while(i < j){
// 从前往后
while (i < j && arr[j] > pivot){
j--;
}
swap(arr, i, j);
// 从后往前
while (i < j && arr[i] < pivot){
i++;
}
swap(arr, i, j);
}
// 当i=j的时候,开始递归,对左右两部分进行快排
quickSort(arr,low,j-1);
quickSort(arr, j+1, high);
}
}
private static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
插入排序 O(n^2)
将n个待排序数组看成一个有序表和一个无序表,最开始有序表有1个元素,无序表有n-1个元素,排序时,每次从无序表中取出第一个元素,插入到有序表中的对应位置(从有序表的最后开始往前一个一个进行比较交换),使之成为新的有序表,重复n-1次即可完成排序。
// 插入排序: 稳定,时间复杂度为O(n^2)
public class InsertSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,0,-2,14};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void insertSort(int[] arr){
for(int i = 1; i < arr.length; i++){
// 取出无序表中的第一个元素arr[i],其前面就是有序表,从后往前逐次进行比较交换,找到其对应位置
for(int j = i; j > 0; j--){
if(arr[j] < arr[j-1]){
swap(arr, j, j-1);
}
else {
break;
}
}
}
}
private static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
希尔排序 O(nlogn)-O(n^2)
希尔排序是对简单插入排序的改进,把数组按照下标的一定增量分组,对每组直接使用插入排序进行排序,随着增量的逐渐减小,每组包含的元素越来越多,当增量减至1时,整个元素被分为一组,算法便终止。
示例:
// 希尔排序: 不稳定(先分组才排序),时间复杂度为O(nlogn)-O(n^2)
public class ShellSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,0,-2,14};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void shellSort(int[] arr){
// 每一个增量(间隔)排序一轮
for(int gap = arr.length / 2; gap > 0; gap /= 2){
// 注意i不从第一个元素开始,因为插入排序是从无序序列的第一元素开始,与前面的元素进行比较交换
for(int i = gap; i < arr.length; i += gap){
for(int j = i; j >= gap; j -= gap){
if(arr[j] < arr[j-gap]){
swap(arr, j, j-gap);
}else {
break;
}
}
}
}
}
private static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
选择排序 O(n^2)
以升序排序为例,把数组也是分为无序序列和有序序列,每次从无序序列中选择出最小的元素,放到有序序列的末尾。
// 选择排序: 不稳定,时间复杂度为O(n^2)
public class SelectionSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,0,-2,14};
selectionSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void selectionSort(int[] arr){
for(int i = 0; i < arr.length-1; i++){
// 选择最小的
int min_index = i;
for(int j = i+1; j < arr.length; j++){
if(arr[j] < arr[min_index]){
min_index = j;
}
}
swap(arr, i, min_index);
}
}
private static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
堆排序 O(nlogn) ★
前置知识:大顶堆、小顶堆;节点i的父节点为(i - 1) / 2,左子孩子为2 * i + 1,右子孩子为2 * i + 2;
步骤:
- 根据初始数组取构造初始堆
- 每次交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下的元素重新调整为大根堆,再进行交换第一个元素和最后一个元素,再调整大顶堆,重复执行,直到整个数组排序完成(堆里只剩下一个元素)。
调整大顶堆:从最后一个非叶子节点(arr.length - 1 - 1) / 2开始,从右至左,从下至上进行调整,调整为大顶堆;
// 堆排序: 稳定,时间复杂度为建队O(n),排序O(nlogn),总为O(nlogn)
public class HeapSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,14,5,-3};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr){
// 初始建堆
for(int i = arr.length/2-1; i >= 0; i--){
heapify(arr, i, arr.length-1);
}
// 堆排序过程
for(int i = arr.length-1; i > 0; i--){
// 交换当前节点和堆顶节点
swap(arr, 0, i);
heapify(arr, 0, i-1);
}
}
private static void heapify(int[] arr, int i, int last_index){
int max = i;
if(2*i+1 <= last_index && arr[2*i+1] > arr[max]){
max = 2*i+1; // 记左节点为最大值
}
if(2*i+2 <= last_index && arr[2*i+2] > arr[max]){
max = 2*i+2; // 记右节点为最大值
}
if(max != i){
swap(arr, i, max);
heapify(arr, max, last_index);
}
}
private static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
归并排序 O(nlogn) ★
分治策略:
-
分:递归拆分子序列,直到拆分成子序列长度为1;
-
治:将两个已经有序的子序列合并成一个有序序列。
示例:
// 归并排序: 稳定,时间复杂度为O(nlogn)
public class MergeSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,0,-2,14};
mergeSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
private static void mergeSort(int[] arr, int low, int high){
if(low < high){
int mid = low + (high - low) / 2;
// 对左边进行归并排序
mergeSort(arr, low, mid);
// 对右边进行归并排序
mergeSort(arr, mid+1, high);
// 合并
merge(arr, low, mid, high);
}
}
private static void merge(int[] arr, int low, int mid, int high){
// 创建一个临时数组,用于存储有序序列
int[] tmp = new int[arr.length];
int i = low;
int j = mid+1;
int k = 0;
while(i <= mid && j <= high){
if(arr[i] <= arr[j]){
tmp[k++] = arr[i++];
}else {
tmp[k++] = arr[j++];
}
}
// 如果左边还有剩余
while (i <= mid){
tmp[k++] = arr[i++];
}
// 如果右边有剩余
while (j <= high){
tmp[k++] = arr[j++];
}
// 把有序序列放回到原数组中
k = 0;
while (low <= high){
arr[low++] = tmp[k++];
}
}
}
计数排序 O(n+k) ★
非比较排序算法,将输入的数据值转化为键存储在额外开辟的数组空间中。要求输入的数据必须是有确定范围的整数!
示例:
// 计数排序: 稳定排序,时间复杂度为O(n+k)
public class CountSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,14,5,-3};
countSort(arr);
System.out.println(Arrays.toString(arr));
}
private static void countSort(int[] arr){
// 先找出数组的最大值
int max = arr[0];
int min = arr[0];
for(int i = 1; i < arr.length; i++){
if(arr[i] > max){
max = arr[i];
}
if(arr[i] < min){
min = arr[i];
}
}
// 创建计数数组
int[] cnt = new int[max-min+1];
for(int num : arr){
cnt[num-min]++;
}
int k = 0;
// 遍历计数数组
for(int i = 0; i < cnt.length; i++){
while (cnt[i]>0){
arr[k++] = i + min;
cnt[i]--;
}
}
}
}
桶排序 O(n) ★
计数排序的改进。根据一定的映射规则/函数,把每一个元素放入到其对应的桶中,再对每一个桶中所有元素进行排序,最后依次输出每个桶中元素,即为有序序列。高效与否就在于这个映射规则。
示例:
// 桶排序: 稳定性取决于其桶内的排序算法稳定性,时间复杂度最好为O(n)
public class BucketSort {
public static void main(String[] args) {
int[] arr = new int[]{5,2,8,3,7,14,5,-3};
bucketSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void bucketSort(int[] arr){
// 先找出数组的最大值和最小值
int max = arr[0];
int min = arr[0];
for(int i = 1; i < arr.length; i++){
if(arr[i] > max){
max = arr[i];
}
if(arr[i] < min){
min = arr[i];
}
}
// 桶的个数
int count = (max - min) / arr.length + 1;
List<ArrayList<Integer>> buckets = new ArrayList<>();
for(int i = 0; i < count; i++){
buckets.add(new ArrayList<Integer>());
}
// 遍历数组,把数据放在对应的桶里
for(int i = 0; i < arr.length; i++){
buckets.get((arr[i] - min) / arr.length).add(arr[i]);
}
// 对每个桶分别进行排序
for(int i = 0; i < buckets.size(); i++){
Collections.sort(buckets.get(i));
}
// 遍历每个桶里的每个数据,输出到arr
int k = 0;
for(int i = 0; i < buckets.size(); i++){
ArrayList<Integer> list = buckets.get(i);
for(int j = 0; j < list.size(); j++){
arr[k++] = list.get(j);
}
}
}
}
] - min) / arr.length).add(arr[i]);
}
// 对每个桶分别进行排序
for(int i = 0; i < buckets.size(); i++){
Collections.sort(buckets.get(i));
}
// 遍历每个桶里的每个数据,输出到arr
int k = 0;
for(int i = 0; i < buckets.size(); i++){
ArrayList<Integer> list = buckets.get(i);
for(int j = 0; j < list.size(); j++){
arr[k++] = list.get(j);
}
}
}
}