序言
排序算法是面试过程中经常会被问到的基础知识,今天,我们来总结一下比较常见的几种排序算法:直接插入排序、希尔排序、冒泡排序、选择排序、快速排序、堆排序、归并排序、桶排序、基数排序。
准备知识
在介绍各种排序算法之前,我们先来熟悉一些概念,包括时间复杂度、空间复杂度、算法稳定性。
1. 时间复杂度
时间频度:一个算法执行所消耗的时间,从理论上是不能算出来的,必须在机器上运行才知道。但我们不可能也没必要对每个算法上机测试,只需要知道那个算法花费时间多,那个花费时间少即可。并且算法花费时间与算法语句执行次数成正比。一个算法中语句执行次数成为语句频度或时间频度,用T(n)表示。
时间复杂度:时间频度T(n)中,n是问题的规模,当n不断变化时,T(n)也会发生变化。现在我们想知道这个变化的规律,因此引入时间复杂度概念。常见的时间复杂度有:常数阶O(1)、线性阶O(n)、线性对数阶O(nlog2n)、平方阶O(n²)、立方阶O(n³)、k次方阶O(nk),指数阶O(2n)。随着规模n的增大,上述时间复杂度不断增大,算法执行效率越低。
2. 空间复杂度
一个算法的空间复杂度是指算法所耗费的存储空间,它也是问题规模n的函数,主要包括存储算法本身占用的空间、算法输入输出数据占用的空间、算法运算过程中临时占用的空间。
3. 算法稳定性
通俗理解,就是保证两个相等的数,排序前和排序后位置保持不变。形式化表示:Ai == Aj,排序前Ai在Aj前面,那么排序后,Ai仍然在Aj前面。
常见算法
1. 直接插入排序
/**
* 名称:1. 插入排序
* 解释:找到当前元素在子数组(包括自身和之前的元素)中的位置。每次把当前元素插入到正确的位置。
* 时间复杂度:O(n²)
* 空间复杂度:O(1)
* 稳定
*/
public static void insertionSort(int[] array) {
if (array == null && array.length == 0) {
return;
}
int i, j, tmp;
for (i = 1; i < array.length; i++) {
tmp = array[i];
for (j = i; j > 0; j--) {
if (tmp < array[j - 1]) {
array[j] = array[j - 1];
continue;
}
break;
}
array[j] = tmp;
}
System.out.println("插入排序结果:");
for (int p = 0; p < array.length; p++) {
System.out.println(array[p]);
}
}
2. 希尔排序
/**
* 名称:2. 希尔排序
* 解释:选择步长gap(gap按一定规则减小,最后为1),i从gap开始向后遍历,每个元素都与(i-gap)所在元素进行比较,直至所有元素排序成功
* 插入排序是希尔排序的特殊情况(gap=1)
* 时间复杂度:平均O(n^1.3),与gap相关
* 空间复杂度:O(1)
* 不稳定
*/
public static void shellSort(int[] array) {
if (array == null && array.length == 0) {
return;
}
int gap, i, j, tmp;
for (gap = array.length / 2; gap > 0; gap = gap / 2) {
for (i = gap; i < array.length; i++) {
tmp = array[i];
for (j = i; j >= gap; j = j - gap) {
if (tmp < array[j - gap]) {
array[j] = array[j - gap];
continue;
}
break;
}
array[j] = tmp;
}
}
System.out.println("希尔排序结果:");
for (int p = 0; p < array.length; p++) {
System.out.println(array[p]);
}
}
3. 冒泡排序
/**
* 名称:3. 冒泡排序,相邻两个比较,反序则交换位置,每次会把(n-1-i)位置的数排好,即每次排好最大的元素
* 与插入排序的区别:插入排序每次把当前元素在它之前的数组找到位置,冒泡排序每次排好最后一个元素
* 时间复杂度:O(n²)
* 空间复杂度:O(1)
* 稳定
*/
public static void bubbleSort(int[] array) {
if (array == null && array.length == 0) {
return;
}
int i, j, tmp;
for (i = 0; i < array.length - 1; i++) {
for (j = 0; j < array.length - 1 - i; j++) {
if (array[j + 1] < array[j]) {
tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
}
}
}
System.out.println("冒泡排序结果:");
for (int p = 0; p < array.length; p++) {
System.out.println(array[p]);
}
}
4. 选择排序
/**
* 名称:4. 选择排序,从i开始,每次从子数组(i->length)中找到最小的元素,和位置i的元素进行交换,即每次排好最小的元素
* 时间复杂度:O(n²)
* 空间复杂度:O(1)
* 不稳定
*/
public static void selectSort(int[] array) {
if (array == null && array.length == 0) {
return;
}
int i, j, min, tmp;
for (i = 0; i < array.length - 1; i++) {
min = array[i];
for (j = i + 1; j < array.length; j++) {
if (array[j] < min) {
tmp = min;
min = array[j];
array[j] = tmp;
}
}
array[i] = min;
}
System.out.println("选择排序结果:");
for (int p = 0; p < array.length; p++) {
System.out.println(array[p]);
}
}
5. 快速排序
/**
* 名称:5. 快速排序,以某个元素为锚点,从右边开始找比锚点小的元素,从左边开始找比锚点大的元素,交换二者,递归进行直至排序完成
* 时间复杂度:O(nlogn)
* 空间复杂度:O(logn),因为递归调用
* 不稳定
*/
public static void quickSort(int[] array, int left, int right) {
if (array == null && array.length == 0) {
return;
}
int i, j, anchor, tmp;
if (left > right) {
return;
}
anchor = array[left];
i = left;
j = right;
while (i < j) {
//从右边开始找到小于anchor的数(必须从右边开始找,因为最后结束的那个元素需要与锚点交换位置,这个元素必须比锚点小)
while (array[j] >= anchor && j > i) {
j--;
}
//从左边开始找到大于anchor的数
while (array[i] <= anchor && i < j) {
i++;
}
//交换两者
if (i < j) {
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
//交换锚点和ij相交元素
array[left] = array[j];
array[j] = anchor;
//递归继续排列左右两边的元素
quickSort(array, left, j - 1);
quickSort(array, j + 1, right);
}
private static void testQuickSort(int[] array) {
quickSort(array, 0, array.length - 1);
System.out.println("快速排序结果:");
for (int p = 0; p < array.length; p++) {
System.out.println(array[p]);
}
}
6. 堆排序
/**
* 名称:6. 堆排序,把待排序数组当做一个完全二叉树(特点left=parent*2+1 right=parent*2+2),然后对二叉树进行一次遍历排序,使得堆顶元素最大,把最大元素放到最后面,然后重复上述步骤
* 时间复杂度:O(nlogn)
* 空间复杂度:O(1)
* 不稳定
*/
public static void heapSort(int[] array) {
if (array == null && array.length == 0) {
return;
}
int p, i, tmp, left, right, maxIndex;
//从末尾开始逐步缩小待排序数组,直至排序完成
for (p = array.length - 1; p > 0; p--) {
//对每个节点进行一次排序,达到所有父节点 > 子节点
for (i = (p + 1) / 2 - 1; i >= 0; i--) {
left = i * 2 + 1;
right = i * 2 + 2;
maxIndex = i;
if (left <= p && array[left] > array[maxIndex]) {
maxIndex = left;
}
if (right <= p && array[right] > array[maxIndex]) {
maxIndex = right;
}
//把虽大元素交换到父节点
if (maxIndex != i) {
tmp = array[i];
array[i] = array[maxIndex];
array[maxIndex] = tmp;
}
}
//把堆顶元素放到数组末尾
tmp = array[0];
array[0] = array[p];
array[p] = tmp;
}
System.out.println("堆排序结果:");
for (int k = 0; k < array.length; k++) {
System.out.println(array[k]);
}
}
7. 归并排序
/**
* 名称:7. 归并排序,把一个很长的数组逐步分成小数组排序,然后又合并为大的数组排序
* 时间复杂度:O(nlogn)
* 空间复杂度:O(n),因为递归调用
* 稳定
*/
public static void mergeSort(int[] array) {
if (array == null && array.length == 0) {
return;
}
internalSort(array, 0, array.length - 1);
System.out.println("归并序结果:");
for (int k = 0; k < array.length; k++) {
System.out.println(array[k]);
}
}
private static void internalSort(int[] array, int left, int right) {
if (left < right) {
int middle = (left + right) / 2;
internalSort(array, left, middle);
internalSort(array, middle + 1, right);
sort(array, left, middle, right);
}
}
private static void sort(int[] array, int left, int middle, int right) {
int i = left;
int j = middle + 1;
int[] tmp = new int[array.length];
int index = left;
while (i <= middle && j <= right) {
if (array[i] < array[j]) {
tmp[index++] = array[i++];
} else {
tmp[index++] = array[j++];
}
}
while (i <= middle) {
tmp[index++] = array[i++];
}
while (j <= right) {
tmp[index++] = array[j++];
}
for (int p = left; p <= right; p++) {
array[p] = tmp[p];
}
}
8. 桶排序
/**
* 名称:8. 桶排序,把最大元素和最小元素的差值划分为若干区间,然后把每个数加入到对应的区间,对每个区间排序,最后把各区间连接起来
* 时间复杂度:O(n+k)
* 空间复杂度:O(n+k)
* 依赖桶内排序算法(如果用到桶内排序用到快排,则不稳定)
*/
public static void bucketSort(int[] array) {
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
//根据数组元素区间获取桶数量
int bucketNum = (max - min) / array.length + 1;
ArrayList> buckets = new ArrayList<>(bucketNum);
for (int i=0; i
buckets.add(new ArrayList());
}
//把元素加入到桶中
for (int i=0; i
buckets.get((array[i] - min)/array.length).add(array[i]);
}
//对每个桶内元素进行排序
for (int i=0; i
Collections.sort(buckets.get(i));
}
System.out.println(buckets.toString());
}
9. 基数排序
/**
* 名称:9. 基数排序,从个位开始(循环到最高位),把每个元素按照取模值放入二维数组对应的位置,一个次循环完毕之后,
* 把二维数组中的元素又放回到原数组中
* 时间复杂度:O(kn)
* 空间复杂度:O(kn)
* 稳定
*/
public static void radixSort(int[] array) {
if (array == null && array.length == 0) {
return;
}
//找到数组中的最大值
int max = array[0];
int length = array.length;
int k = 1; //用来取各个位上的数字
int index = 0;
for (int i = 1; i < length; i++) {
if (max < array[i]) {
max = array[i];
}
}
int[][] buckets = new int[10][length];
int[] counts = new int[10];
int digit;
while (k <= max) {
//把元素添加到二维数组中
for (int i=0; i
digit = (array[i]/k) % 10;
buckets[digit][counts[digit]++] = array[i];
}
//把二维数组中的元素顺序放入数组中
for (int i=0; i<10; i++) {
if (counts[i] > 0) {
for (int j=0; j < counts[i]; j++) {
array[index++] = buckets[i][j];
}
}
counts[i] = 0;
}
k *= 10;
index = 0;
}
System.out.println("基数排序结果:");
for (int p = 0; p < array.length; p++) {
System.out.println(array[p]);
}
}
总结
上面给出了各种排序算法的实现,并且每个算法都给了比较详细的注释,各位同学可以参考注解理解代码,这里给出各算法的一览图:
image.png