排序算法
排序算法种类
插入排序
- 直接插入排序
- 希尔排序
选择排序
- 简单选择排序
- 堆排序
交换排序
- 冒泡排序
- 快速排序
归并排序
基数排序
排序算法时间复杂度
冒泡排序
通过对待排序序列从前向后(从下标较小元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前向后移动,像气泡一样逐渐往上冒
给定数组[5,3,9,2,4]
- 第一趟排序后结果为:[3,5,2,4,9]
- 第二趟排序后结果为:[3,2,4,5,9]
- 第三趟排序后结果为:[2,3,4,5,9]
public static void bubbleSort(int[] arr) {
// 冒泡排序,时间复杂度O(n^2)
int temp = 0;
boolean flag = false;// 优化冒泡排序,发现有一趟排序之后没有任何一个数据交换,则退出循环
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (!flag) {
break;
} else {
flag = false; //如果发生交换,重置flag值
}
}
}
选择排序
每次都选出数组最小值,设置一个辅助索引,与辅助索引所在位置的值进行交换进行交换
给定数组[5,3,9,2,4]
- 第一趟排序后结果为:[2,3,9,5,4]
- 第二趟排序后结果为:[2,3,9,5,4]
- 第三趟排序后结果为:[2,3,4,5,9]
public static void selectSort(int[] arr) {
//选择排序,时间复杂度O(n^2)
for (int i = 0; i < arr.length-1; i++) {
int minIndex = i; // 最小值的下标
int min = arr[i]; // 最小值
for (int j = i+1; j < arr.length; j++) {
if (min > arr[j]){ // 找到比假设的最小值更小元素,记录最小值以及下标
min = arr[j];
minIndex = j;
}
}
if (minIndex != i){ // 将每一轮找到的最小值与数组的第i个元素进行替换
arr[minIndex] =arr[i];
arr[i] = min;
}
}
}
插入排序
将待排序数组看作一个有序数组和无须数组,依次将无序数组的第一个元素插入到有序数组中的适当位置
给定数组[5,3,9,2,4]
- 第一趟排序后结果为:[3,5,9,2,4]
- 第二趟排序后结果为:[3,5,9,2,4]
- 第三堂排序后结果为:[2,3,5,9,4]
- 第四趟排序后结果为:[2,3,4,5,9]
public static void insertSort(int[] arr) {
int insertVal = 0;
int insertIdx = 0;
for (int i = 1; i < arr.length; i++) {
insertVal = arr[i];
insertIdx = i - 1;
// 保证insertIdx不会越界
while (insertIdx >= 0 && insertVal < arr[insertIdx]) {
arr[insertIdx + 1] = arr[insertIdx];
insertIdx--;
}
if (insertIdx + 1 != i) {
arr[insertIdx + 1] = insertVal;
}
// System.out.println("第"+i+"趟排序后的结果:"+ Arrays.toString(arr));
}
}
希尔(shell)排序
先将待排序数组按步长gap分组,然后对每一组中的元素进行排序,循环此动作直到gap无限接近于0
给定数组[5,3,9,2,4]
- 第一趟排序后结果为:[5,2,9,3,4]
- 第二趟排序后结果为:[2,3,4,5,9]
/**
* 交换型shell排序,效率不高,因为是用交换法对分组后的数据进行排序
* shell排序的平均时间复杂度为nlogn,最坏时间复杂度为n^s(1<s<2)
*
* @param arr
*/
public static void shellSort1(int[] arr) {
int temp = 0;
int count = 0;
// 根据分析进行循环处理
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}
/**
* 优化版希尔排序,使用插入法对分组的数据进行排序
* @param arr
*/
public static void shellSort2(int[] arr) {
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
// 保证insertIdx不会越界
while (j - gap >= 0 && temp < arr[j - gap]) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
快速排序
在数组中先找到一个基准值以及位置,然后从数组的左边和右边分别开始查找,找到比基准值小的放左边,比基准值大的放右边,直到基准值左边所有元素都小于右边元素,然后对左边和右边元素进行递归
给定数组[5,3,9,2,4],以中轴值为基准
-
第一轮交换的结果为:[5,3,4,2,9]
-
第二轮交换的结果为:[2,3,4,5,9]
public static void quickSort(int[] arr, int left, int right) {
int l = left;
int r = right;
// 中轴值
int pivot = arr[(right + left) / 2];
int temp = 0;
while (l < r) {
//从数组左边开始一直找,直到找到一个大于等于pivot的值
while (arr[l] < pivot) {
l += 1;
}
//从数组右边一直找,直到找到一个小于等于pivot的值
while (arr[r] > pivot) {
r -= 1;
}
// 如果l>=r,说明左边的数全部小于右边的数,退出循环
if (l >= r) {
break;
}
// 交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交换完之后,发现这个arr[l] == pivot,r-- 前移
if (arr[l] == pivot) {
r -= 1;
}
//如果交换完之后,发现这个arr[r] == pivot,l++ 后移
if (arr[r] == pivot) {
l += 1;
}
}
// 如果l==r,必须l++,r--,否则会出现栈溢出
if (l == r) {
l += 1;
r -= 1;
}
// 如果中轴值左边有数据,递归
if (right > l) {
quickSort(arr, l, right);
}
// 如果中轴值右边有数据,递归
if (left < r) {
quickSort(arr, left, r);
}
}
归并排序
将数组从中间拆分成两部分,一直分拆到数组元素为1,然后合并,合并时按顺序合并,一直递归直到合并完成
给定数组[5,3,9,2,4]
- 第一次合并:[3,5,9,2,4](以9为中轴拆分成左右两部分)
- 第二次合并:[2,3,4,5,9](合并)
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (right + left) / 2;
// 向左递归分解
mergeSort(arr, left, mid, temp);
// 向右递归分解
mergeSort(arr, mid + 1, right, temp);
// 合并
merge(arr, left, mid, right, temp);
}
}
/**
* arr数组分解之后合并步骤
*
* @param arr 原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 辅助数组,用于存放中间数据
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int foot = 0;// 记录temp数组当前插入位置索引
int i = left;// 初始化i,左边有序序列的初始索引
int j = mid + 1;// 初始化j,右边有序序列的初始索引
// 第一步:
// 先把左右两边(有序)的数据按照规则填充到temp数组
// 直到有一边的数据处理完毕
while (i <= mid && j <= right) {
// 如果左边数组的当前元素小于等于右边数组当前元素,则将左边数组元素拷贝到temp,反之亦然
if (arr[i] <= arr[j]) {
temp[foot] = arr[i];
i += 1;
foot += 1;
} else {
temp[foot] = arr[j];
j += 1;
foot += 1;
}
}
// 第二步
// 将有剩余数据的数组数据全部填充到temp
while (i <= mid) {
temp[foot] = arr[i];
i += 1;
foot += 1;
}
while (j <= right) {
temp[foot] = arr[j];
j += 1;
foot += 1;
}
// 第三步
// 将temp数组的元素拷贝到arr中
int t = 0;
int tempLeft = left;// 因为每一次分拆后的数组不一定是是合并所有元素,每一次分拆后的元素合并个数不一样
// System.out.println("tempLeft:" + tempLeft + "right:" + right);
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
基数排序(桶排序)
准备一个length为10的二维数组bucket,然后循环遍历需要排序的数组,依次按数组中元素的个,十,百…位数字放入到bucket对应的数组中,若元素高位为空,则补0,然后遍历bucket,按顺序将bucket中元素取出来,重复上述操作直至数组中元素的所有位数都比较完成。
给定数组[5,3,9,2,4]
- 第一次放入桶中:[2,3,4,5,9]
// 由于基数排序是典型的用空间换时间的算法
// 所以基数排序会耗费额外的内存空间,所以当数组长度过大的时候,可能会出现OutOfMemoryError错误
public static void radixSort(int[] arr) {
// 获取数组中最大元素
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 得到最大元素位数
int maxLength = (max + "").length();
// 定义一个二维数组,表示有10个桶,每个桶的就是一个一位数组
// 说明
// 1.二维数组包含10个一位数组
// 2.因为无法确定元素中对应位数相等的有多少,所以初始化bucket中桶的大小为arr.length
// 3.很明确,基数排序是用空间换时间的经典算法
int[][] bucket = new int[10][arr.length];
// 定义一个一位数组记录每个桶中每次放入的元素个数
int[] bucketElementCounts = new int[10];
// 通过循环来处理每一轮排序
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
for (int j = 0; j < arr.length; j++) {
// 取出每个元素个位数的值
int digitOfElement = arr[j] / n % 10;
// 放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
// 更新记录元素个数的数组
bucketElementCounts[digitOfElement]++;
}
int index = 0;
// 按顺序取出桶中所有数据
for (int k = 0; k < bucket.length; k++) {
if (bucketElementCounts[k] > 0) {
for (int l = 0; l < bucketElementCounts[k]; l++) {
arr[index++] = bucket[k][l];
}
}
// 每一个桶中数据取出之后必须bucketElementCounts[k] = 0,否则会影响下一轮排序
bucketElementCounts[k] = 0;
}
}
}
特别注意:基数排序是典型的使用空间换时间的算法,如果数组过大,会导致OutOfMemoryError错误