一次性精进“排序算法”

365 阅读5分钟

总览

本文主要讲述经典的10种排序算法,包括:概括,图解以及代码实现。

本文不仅适用于正在学习排序算法的初学者,也适用于有工作经验的码农的自我精进。

名称时间复杂度空间复杂度是否稳定
选择O(n2)O(1)非稳定
插入O(n2)O(1)稳定
冒泡O(n2)O(1)稳定
希尔O(nlogn)O(1)非稳定
归并O(nlogn)O(n)稳定
快速O(nlogn)O(1)非稳定
堆排序O(nlogn)O(1)稳定
计数排序O(n + k)O(n + k)稳定
桶排序O(n + k)O(n + k)稳定
基数排序O(n * k)O(n * k)稳定

选择排序

概述:

每次遍历非有序数列,选择最小的数非有序数列的第一个数交换,此时非有序数列逐渐缩短,直至全部有序。

private int[] selectSort(int []arrays){
    int length = arrays.length;
    for(int i=0;i<length-1;i++){
        int min = i;
        for(int j=i;j<length;j++){
            if(arrays[min]>arrays[j]){
                min = j;
            }
        }
        int tmp = arrays[i];
        arrays[i] = arrays[min];
        arrays[min] = tmp;
    }
    return arrays;
}

插入排序

概述:

从非有序数列中,每次取出第一个数,将该数插入到有序数列当中,直至非有序数列取完为止。

private int[] insertSort(int arrays[]){
    int length = arrays.length;
    if(length <= 1){
        return arrays;
    }
    for(int i=1;i<length;i++){
        int insertNum = arrays[i];
        for(int j=i-1;j>=0;j--){
            if(arrays[j]>insertNum){
                arrays[j+1] = arrays[j];
                arrays[j] = insertNum;
            }
        }
    }
    return arrays;
}

冒泡排序

概况:每次遍历非有序数列,对其相邻的数两两比较,每次遍历就能将最大数/最小数“冒泡”到最后,非有序数列逐渐变小直至全部有序。

private int[] bubbleSort(int arrays[]){
	int length = arrays.length;
	for(int i=length;i>=0;i--){
		for(int j=0;j<i-1;j++){
			if(arrays[j]>arrays[j+1]){
				int tmp = arrays[j+1];
				arrays[j+1] = arrays[j];
				arrays[j] = tmp;
			}
		}
	}
	return arrays;
}

希尔排序

概述:希尔排序可以理解为插入排序和冒泡排序的改进版,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。

希尔排序采用了分治的思想,使得外层循环时间复杂度从O(n)改进成O(logn),总时间复杂度O(nlogn)。

    private int[] shellSort(int arrays[]){
        int n = arrays.length;
        for(int step = n/2; step > 0;step /= 2) 
        {
            for(int i = step; i < n; i++)
            {
                int tmp = arrays[i];
                int j = i - step;
                for(;j >= 0 && tmp < arrays[j];)
                {
                    arrays[j + step] = arrays[j];
                    j -= step;
                }
                arrays[j + step] = tmp;
            }
        }
        return arrays;
    }

归并排序

概述:

归并排序使用分治的思想,

首先定义一个mergeSort(arr,left,right),left<right为对arr数组从left到right的排序

那么可以得出递推公式:mergeSort(arr,left,right) = mergeSort(arr,left,mid)+mergeSort(arr,mid+1,right),mid=(left+right)/2;终止条件:left>=right。

此时arr数组leftmid有序,mid+1right有序,定义merge(arr,left,mid,right),将两个有序的数合并。

private void mergeSort(int arrays[],int left,int right){
	if(left>=right){
		return;
	}
	int mid = (left+right)/2;
	mergeSort(arrays,left,mid);
	mergeSort(arrays,mid+1,right);
	merge(arrays, left, mid,right);
}


private void merge(int []arrays,int left,int mid,int right){
	int [] tmp = Arrays.copyOf(arrays,arrays.length);
	int i = left;
	int j = mid+1;
	int index = i;
	while(i<=mid&j<=right){
		if(tmp[i]<tmp[j]){
			arrays[index++] = tmp[i];
			i++;
		}else{
			arrays[index++] = tmp[j];
			j++;
		}
	}
	for(;i<=mid;i++){
		arrays[index++] = tmp[i];
	}
	for(;j<=right;j++){
		arrays[index++] = tmp[j];
	}
}


快速排序

概述:

快速排序是通过多次比较和交换来实现排序,在一趟排序中把将要排序的数据分成两个独立的部分,对这两部分进行排序使得其中一部分所有数据比另一部分都要小,然后继续递归排序这两部分,最终实现所有数据有序。

private void quickSort(int arrays[],int left,int right){
	if(left>=right){
		return;
	}
	if (left < right) {
		int partitionIndex = partition(arrays, left, right);
		quickSort(arrays, left, partitionIndex - 1);
		quickSort(arrays, partitionIndex + 1, right);
	}
}

private int partition(int[] arr, int left, int right) {
	// 设定基准值(pivot)
	int pivot = left;
	int index = pivot + 1;
	//index 记录比pivot大的数的下标,i遇到比pivot小的数的,将i和index进行交换
	for (int i = index; i <= right; i++) {
		if (arr[i] < arr[pivot]) {
			swap(arr, i, index);
			index++;
		}
	}
	swap(arr, pivot, index - 1);
	return index - 1;
    }

堆排序

概述:堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)。

public int[] heapSort(int[] sourceArray) throws Exception {
	// 对 arr 进行拷贝,不改变参数内容
	int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
	
	int len = arr.length;
	
	buildMaxHeap(arr, len); // 构建大顶堆
	
	for (int i = len - 1; i > 0; i--) {
		swap(arr, 0, i); 
		len--;
		heapify(arr, 0, len);
	}
	return arr;
}

// 构建大顶堆
private void buildMaxHeap(int[] arr, int len) {
	for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
		heapify(arr, i, len);
	}
}

//堆调整
private void heapify(int[] arr, int i, int len) {
	int left = 2 * i + 1;
	int right = 2 * i + 2;
	int largest = i;
	
	if (left < len && arr[left] > arr[largest]) {
		largest = left;
	}
	
	if (right < len && arr[right] > arr[largest]) {
		largest = right;
	}
	
	if (largest != i) {
		swap(arr, i, largest);
		heapify(arr, largest, len);
	}
}

private void swap(int[] arr, int i, int j) {
	int temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
    }

计数排序

概述:计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

适用场景:计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

public int[] sort(int[] sourceArray) throws Exception {
	// 对 arr 进行拷贝,不改变参数内容
	int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
	
	int maxValue = getMaxValue(arr); // 求最大值,形成区间
	
	return countingSort(arr, maxValue);
}

private int[] countingSort(int[] arr, int maxValue) {
	int bucketLen = maxValue + 1;
	int[] bucket = new int[bucketLen];
	
	for (int value : arr) {
		bucket[value]++;
	}
	
	int sortedIndex = 0;
	for (int j = 0; j < bucketLen; j++) {
		while (bucket[j] > 0) {
			arr[sortedIndex++] = j;
			bucket[j]--;
		}
	}
	return arr;
}

private int getMaxValue(int[] arr) {
	int maxValue = arr[0];
	for (int value : arr) {
		if (maxValue < value) {
			maxValue = value;
		}
	}
	return maxValue;
}

桶排序

概述:桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

元素分布在桶中:

然后,元素在每个桶中排序:

public class BucketSort implements IArraySort {

    private static final InsertSort insertSort = new InsertSort();

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        return bucketSort(arr, 5);
    }

    private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
        if (arr.length == 0) {
            return arr;
        }

        int minValue = arr[0];
        int maxValue = arr[0];
        for (int value : arr) {
            if (value < minValue) {
                minValue = value;
            } else if (value > maxValue) {
                maxValue = value;
            }
        }

        int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
        int[][] buckets = new int[bucketCount][0];

        // 利用映射函数将数据分配到各个桶中
        for (int i = 0; i < arr.length; i++) {
            int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
            buckets[index] = arrAppend(buckets[index], arr[i]);
        }

        int arrIndex = 0;
        for (int[] bucket : buckets) {
            if (bucket.length <= 0) {
                continue;
            }
            // 对每个桶进行排序,这里使用了插入排序
            bucket = insertSort.sort(bucket);
            for (int value : bucket) {
                arr[arrIndex++] = value;
            }
        }

        return arr;
    }

    /**
     * 自动扩容,并保存数据
     *
     * @param arr
     * @param value
     */
    private int[] arrAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }

}

基数排序

概述:基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

/**
 * 基数排序
 * 考虑负数的情况还可以参考: https://code.i-harness.com/zh-CN/q/e98fa9
 */
public class RadixSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 对 arr 进行拷贝,不改变参数内容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxDigit = getMaxDigit(arr);
        return radixSort(arr, maxDigit);
    }

    /**
     * 获取最高位数
     */
    private int getMaxDigit(int[] arr) {
        int maxValue = getMaxValue(arr);
        return getNumLenght(maxValue);
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

    protected int getNumLenght(long num) {
        if (num == 0) {
            return 1;
        }
        int lenght = 0;
        for (long temp = num; temp != 0; temp /= 10) {
            lenght++;
        }
        return lenght;
    }

    private int[] radixSort(int[] arr, int maxDigit) {
        int mod = 10;
        int dev = 1;

        for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
            // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
            int[][] counter = new int[mod * 2][0];

            for (int j = 0; j < arr.length; j++) {
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }

            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
        }

        return arr;
    }

    /**
     * 自动扩容,并保存数据
     *
     * @param arr
     * @param value
     */
    private int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }
}

注:

本文的框架主要包括各类排序算法,包括概念,图解和代码。

后续本文的优化的方向:

1、各个算法的应用场景及相互关系

2、工业级排序如何使用上述算法