从要排序序列的第一个元素开始,不断比较相邻元素的值,发现逆序则交换,将值较大的元素逐渐从前向后移动。
每找到待排序序列的最大值时,就将该最大值固定在待排序序列的尾部,且每找到一个待排序序列最大值需要循环一次,n 个值则需要循环 n 次,但最后一个值无需比较,则实际需循环 n-1 次,即 i < arr.length - 1
。
public void bubbleSort (int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
当然,该方法还可以进行优化。
定义一个布尔变量 flag
,用来标记每轮是否进行了交换。在每轮遍历开始时,将 flag
设置为 false。若当轮没有发生交换,即此时 flag
依然为 false,说明此时数组已经按照升序排列。此时外层循环直接退出,排序结束。
public void bubbleSort (int[] arr) {
boolean flag = false;
for (int i = 0; i < arr.length - 1; i++) {
flag = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true; //该轮发生交换
}
}
if (!flag) {
break; //数组已有序
}
}
}
复杂度分析:
选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。同样,选择排序也需要比较 n - 1
轮,最后一轮无需比较。
public void selectSort (int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i; //初始化最小值索引
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
复杂度分析:
把 n 个待排序的元素看成为一个有序表
和一个无序表
,开始时 有序表 中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,与有序表中的元素进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
public void insertSort (int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i]; //记录待排序的数值
int j = i;
while (j > 0 && arr[j - 1] > temp) {
arr[j] = arr[j - 1];
j--;
}
if (j != i) {
arr[j] = temp;
}
}
}
复杂度分析:
希尔排序,也被称为递减增量排序,是直接插入排序的一种改进版本。简单插入排序可能存在的问题:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。
public void shellSort(int[] arr) {
int length = arr.length;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
int temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
}
复杂度分析:
归并排序的核心思想是分治法,把一个复杂问题拆分成若干个子问题来求解。
归并排序的算法思想是:把数组从中间划分为两个子数组,一直递归地把子数组划分成更小的数组。长度为 1 序列是有序的,因此应递归分解直到子数组里面只有一个元素的时候开始排序。排序的方法就是按照大小顺序合并两个元素。接着依次按照递归的顺序返回,不断合并排好序的数组,直到把整个数组排好序。
public static void mergeSort(int[] source, int[] temp, int left, int right) {
if (left >= right) {
return;
}
int mid = (left + right) / 2;
mergeSort(source, temp, left, mid);
mergeSort(source, temp, mid + 1, right);
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (source[i] <= source[j]) {
temp[k++] = source[i++];
} else {
temp[k++] = source[j++];
}
}
while (i <= mid) {
temp[k++] = source[i++];
}
while (j <= right) {
temp[k++] = source[j++];
}
for (i = left, j = 0; i <= right; i++, j++) {
source[i] = temp[j]; //复制回原数组
}
}
public static void main(String[] args) {
int[] arr = {12, 3, 15, 55, 97, 6, 11, 41, 88, 12};
int[] temp = new int[arr.length];
mergeSort(arr, temp, 0, arr.length - 1);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
复杂度分析:
快速排序也采用了分治的思想,如果说归并排序是先拆分再排序,那么快速排序就是先排序再划分。
快速排序的基本思想是:首先从待排序列中选定一个记录,称之为 枢纽
,通过关键字与枢纽的比较将待排序列的序列划分成位于枢纽前后的两个子序列,其中枢纽之前的子序列的所有关键字都不大于枢纽,枢纽之后的子序列的所有关键字都不小于枢纽;此时枢纽已到位,再按同样方法对这两个子序列分别递归进行快速排序,最终使得整个序列有序。
public void quickSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int pivot = arr[left];
int i = left, j = right;
while (i < j) {
while (i < j && arr[j] >= pivot)
j--;
arr[i] = arr[j];
while (i < j && arr[i] <= pivot)
i++;
arr[j] = arr[i];
}
arr[i] = pivot;
quickSort(arr, left, i - 1);
quickSort(arr, j + 1, right);
}
复杂度分析:
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一类完全二叉树,具有以下特性:
大顶堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值
图源:https://www.cnblogs.com/chengxiao/p/6129630.html
堆排序基本步骤:
以数组首元素索引是0还是1区分,算法有以下两个版本:
//数组第 0 个位置不能有元素
public void heapSort(int[] arr) {
int N = arr.length - 1;
//构建大顶堆
for (int i = N / 2; i > 0; i--) {
sink(arr, i, N);
}
while (N > 1) {
swap(arr, 1, N--); //堆顶与堆尾结点交换,堆长度减1
sink(arr, 1, N); //筛选新的堆顶结点
}
}
private void sink(int[] arr, int pos, int N) {
while (pos <= N / 2) {
int j = 2 * pos;
if (j < N && arr[j] < arr[j + 1]) {
j++; // j 为左、右孩子中优先者的位置
}
if (arr[j] < arr[pos]){
return;
}
swap(arr, pos, j);
pos = j;
}
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//数组第 0 个位置有元素
public static void heapSort(int[] arr) {
int N = arr.length;
//构建大顶堆
for (int i = N / 2 - 1; i >= 0; i--) {
sink(arr, i, N);
}
while (N > 1) {
swap(arr, 0, --N); //堆顶与堆尾结点交换,堆长度减1
sink(arr, 0, N); //筛选新的堆顶结点
}
}
//下沉
private static void sink(int[] arr, int pos, int N) {
while (pos <= N / 2 - 1) {
int j = 2 * pos + 1;
if (j < N - 1 && arr[j] < arr[j + 1]) {
j++; // j 为左、右孩子中优先者的位置
}
if (arr[j] < arr[pos]){
return;
}
swap(arr, pos, j);
pos = j;
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
复杂度分析:
堆排序是一种原地排序,没有利用额外的空间。
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
比较和非比较的区别
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度低,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
适用条件:计数排序需要占用大量空间,它仅适用于数据比较集中的情况,如[0,100],高考学生成绩。
//当输入的元素是 n 个 0 到 k 之间的整数
public void countSort(int[] arr) {
int maxValue = arr[0];
//获取最大值
for (int num : arr) {
if (maxValue < num) {
maxValue = num;
}
}
int[] bucket = new int[maxValue + 1];
for (int num : arr) {
bucket[num]++;
}
for (int i = 0, j = 0; i <= maxValue; i++) {
while (bucket[i]-- > 0) {
arr[j++] = i;
}
}
}
当然,上面是最基本的计数排序,还可以有优化的地方。比如开辟的bucket数组的长度可以是最大值与最小值的差值+1,不一定要求数据必须从0开始。
复杂度分析:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
什么时候最快:
当输入的数据可以均匀的分配到每一个桶中。
什么时候最慢:
当输入的数据被分配到了同一个桶中。
public void bucketSort(int[] arr){
// 计算最大值与最小值
int max = arr[0];
int min = arr[0];
for(int num : arr){
if (num > max) {
max = num;
} else if (num < min) {
min = num;
}
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
List<List<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 利用映射函数将数据分配到各个桶中
for(int i = 0; i < arr.length; i++){
int hash = (arr[i] - min) / (arr.length);
bucketArr.get(hash).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
}
复杂度分析:
基数排序是一种非比较型排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。 这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
public void radixSort(int[] arr) {
//定义桶数组,共10个桶,每个桶是一个一维数组
int[][] bucket = new int[10][arr.length];
//每个桶放入元素的个数
int[] bucketCount = new int[10];
int maxValue = arr[0];
//获取最大值
for (int num : arr) {
if (num > maxValue) {
maxValue = num;
}
}
//获取最大位数
int maxLength = (maxValue + "").length();
//在对应位上进行排序
for (int digit = 0, n = 1; digit < maxLength; digit++, n*= 10) {
for (int i = 0; i < arr.length; i++) {
//获取元素对应位的值
int value = arr[i] / n % 10;
bucket[value][bucketCount[value]++] = arr[i];
}
int index = 0;
//将桶中数据放回原数组
for (int j = 0; j < 10; j++) {
for (int k = 0; k < bucketCount[j]; k++) {
arr[index++] = bucket[j][k];
}
bucketCount[j] = 0; // 清0
}
}
}
复杂度分析:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异: