一、冒泡排序
代码实现🎆
public int[] bubbleSort(int[] nums) {
// 未排序数据的右边界
int j = nums.length - 1;
do {
//定义一个变量 x 记录未排序区域的右边界
int x = 0;
for (int i = 0; i < j; i++) {
if (nums[i] > nums[i + 1]) { // 相邻元素中前面的元素大则交换
int t = nums[i];
nums[i] = nums[i + 1];
nums[i + 1] = t;
// 每次交换后,更新变量x,最后一次交换后x的值为经过一轮排序后新的右边界,优化代码
x = i;
}
}
j = x;
} while (j != 0);
return nums;
}
public void bubbleSort(int[] nums, int j) {
if (j == 0) {
return;
}
//定义一个变量 x 记录未排序区域的右边界
int x = 0;
for (int i = 0; i < j; i++) {
if (nums[i] > nums[i + 1]) { // 相邻元素中前面的元素大则交换
int t = nums[i];
nums[i] = nums[i + 1];
nums[i + 1] = t;
// 每次交换以后,更新x <未排序数据的右边界> 的值
x = i;
}
}
bubbleSort(nums, x);
}
public static int[] bubbleSort(int[] arr) {
// 数组长度如果为5,则需要进行4轮冒泡
for (int i = 1; i < arr.length; i++) {
boolean flag = true;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {// 相邻元素中前面的元素大则交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
// Change flag 只要一轮循环发生了交换,则把flag置为false
flag = false;
}
}
// 一轮都没有发生交换,没必要继续,直接退出循环
if (flag) {
break;
}
}
return arr;
}
此处对代码做了一个小优化,加入了 is_sorted
Flag,目的是将算法的最佳时间复杂度优化为 O(n),即当原输入序列就是排序好的情况下,该算法的时间复杂度就是 O(n)。
二、选择排序
代码实现🎆
public void selectSort(int[] nums) {
// 1. 选择轮数:nums.length - 1
// 2. 交换的索引right初始化为nums.length - 1,每次递减,将选择到的最大值与right位置交换
for (int right = nums.length - 1; right > 0; right--) {
int max = right;
for (int i = 0; i < right; i++) {
if (nums[i] > nums[max]) {
max = i;
}
}
// max 值发生了变化才交换,否则不需要交换
if (max != right) {
// 每一轮循环结束时,将选择到的最大的的值交换到最右侧
int t = nums[max];
nums[max] = nums[right];
nums[right] = t;
}
}
}
public void selectionSort(int[] arr) {
// 每一轮循环结束时,将选择到的最小的的值交换到最左侧 同上
for (int left = 0; left < arr.length - 1; left++) {
int minIndex = left;
for (int j = left + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != left) {
int tmp = arr[left];
arr[left] = arr[minIndex];
arr[minIndex] = tmp;
}
}
}
三、插入排序
代码实现🎆
public void insertionSort(int[] nums) {
// 定义一个low指针,指向未排序区域的第一个数,默认从1开始,认为第一个数已经有序
for (int notSortedFirstIndex = 1; notSortedFirstIndex < nums.length; notSortedFirstIndex++) {
// 定义一个指针指向已排序区域的最后一个指针
int sortedLastIndex = notSortedFirstIndex - 1;
// 记录未排序区域的第一个值,用于后序插入
int current = nums[notSortedFirstIndex];
// 如果以排序区域的最后一个值大于未排序区域的第一个值,则将已排序区域的最后一个值向后移动
while (sortedLastIndex >= 0 && nums[sortedLastIndex] > current) {
nums[sortedLastIndex + 1] = nums[sortedLastIndex];
sortedLastIndex--;
}
// 循环结束,说明已经找到插入位置
if (sortedLastIndex != notSortedFirstIndex - 1) {
// 上面的i是经过--以后不满足while条件退出循环,因此在插入时需要在i+1位置插入
nums[sortedLastIndex + 1] = current;
}
}
}
/* 插入排序 */
void insertionSort(int[] nums) {
// i=>未排序区域的第一个指针
// j=>以排序区域的最后一个指针
// base=>暂存未排序区域中选择的基准元素
// 外循环:已排序区间为 [0, i-1]
for (int i = 1; i < nums.length; i++) {
int base = nums[i], j = i - 1;
// 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
j--;
}
nums[j + 1] = base; // 将 base 赋值到正确位置
}
}
四、希尔排序
代码实现🎆
public void shellSort(int[] nums) {
// 定义分组间隙 gap ,初始为数组长度的一半,每次循环右位移一位(除以二)
for (int gap = nums.length >> 1; gap >= 1; gap = gap >> 1) {
// notSortedFirstIndex 未排序数组的第一个指针
for (int notSortedFirstIndex = gap; notSortedFirstIndex < nums.length; notSortedFirstIndex++) {
// 未排序区域的第一个元素值
int current = nums[notSortedFirstIndex];
int sortedLastIndex = notSortedFirstIndex - gap; // 排序数组的最后一个数据指针
// 插入排序的过程
while (sortedLastIndex >= 0 && nums[sortedLastIndex] > current) {
nums[sortedLastIndex + gap] = nums[sortedLastIndex];
sortedLastIndex -= gap;
}
if (sortedLastIndex != notSortedFirstIndex - gap) {
nums[sortedLastIndex + gap] = current;
}
}
}
}
public void shellSort(int[] arr) {
// 定义分组间隙 gap ,初始为数组长度的一半
int gap = arr.length >> 1;
while (gap > 0) {
for (int i = gap; i < arr.length; i++) {
// 未排序区域的第一个元素值
int current = arr[i];
// 已排序区域的最后一个元素指针
int preIndex = i - gap;
// Insertion sort
while (preIndex >= 0 && arr[preIndex] > current) {
// 后移
arr[preIndex + gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex + gap] = current;
}
// 取下一个希尔增量
gap = gap >> 1;
}
}
五、归并排序
代码实现🎆
public void mergeSort(int[] nums) {
int[] arr = new int[nums.length];
/*
* 参数一:待排序数组
* 参数二:待排序数组的区域左标
* 参数二:待排序数组的区域右标
* 参数三:临时数组,用来合并数组时使用
*/
split(nums, 0, nums.length - 1, arr);
}
private void split(int[] nums, int left, int right, int[] arr) {
// 治
if (left == right) {
return;
}
// 分
int mid = (left + right) >>> 1;
split(nums, left, mid, arr);
split(nums, mid + 1, right, arr);
// 合
merge(nums, left, mid, mid + 1, right, arr);
System.arraycopy(arr, left, nums, left, right - left + 1);
}
/**
* 合并有序数组
*
* @param nums 原始数组
* @param i iEnd 第一个有序范围
* @param j jEnd 第二个有序范围
* @param arr 临时数组
*/
private void merge(int[] nums, int i, int iEnd, int j, int jEnd, int[] arr) {
// 定义一个指针k用于构建临时数组
int k = i;
while (i <= iEnd && j <= jEnd) {
// 每次将 i ~ iEnd 或者 j ~ jEnd 中更小的数加入到临时数组,并将指针后移
if (nums[i] < nums[j]) {
arr[k] = nums[i];
i++;
} else {
arr[k] = nums[j];
j++;
}
k++;
}
// 当 i ~ iEnd 或 j ~ jEnd 其中一个区域的数据全部被加入到临时数组,则把另一个区域的数据拷贝到临时数组中
if (i > iEnd) {
System.arraycopy(nums, j, arr, k, jEnd - j + 1);
}
if (j > jEnd) {
System.arraycopy(nums, i, arr, k, iEnd - i + 1);
}
}
public void mergeSort(int[] nums) {
int len = nums.length;
int[] arr = new int[len];
// width 代表有序区间的宽度,取值依次为 1,2,4 ...
for (int width = 1; width < len; width *= 2) {
// [left,right] 分别代表合并区间的左右边界
// 每次合并两个有序区间,因此左边界每次循环 + 2 * width
for (int left = 0; left < len; left += 2 * width) {
//右边界为下次左边界 - 1
int right = Math.min(len - 1, left + 2 * width - 1);
//中间值mid为左边界 + 一个宽度width - 1
int mid = Math.min(len - 1, left + width - 1);
merge(nums, left, mid, mid + 1, right, arr);
}
System.arraycopy(arr, 0, nums, 0, len);
}
}
/**
* 合并有序数组
*
* @param nums 原始数组
* @param i iEnd 第一个有序范围
* @param j jEnd 第二个有序范围
* @param arr 临时数组
*/
private void merge(int[] nums, int i, int iEnd, int j, int jEnd, int[] arr) {
// 定义一个k操作临时数组
int k = i;
//如果i和j都在有效范围内
while (i <= iEnd && j <= jEnd) {
//比较i和j处索引的数组的值,并把较小的值加入到临时数组a2中
if (nums[i] < nums[j]) {
arr[k] = nums[i];
i++;
} else {
arr[k] = nums[j];
j++;
}
//更新操作临时数组的指针
k++;
}
// 当i > iEnd说明第一个有序范围内的元素已经全部迭代,将第二范围内没有被迭代的元素拷贝到arr数组即可
if (i > iEnd) {
System.arraycopy(nums, j, arr, k, jEnd - j + 1);
}
// 当j > jEnd说明第二个有序范围内的元素已经全部迭代,将第一范围内没有被迭代的元素拷贝到arr数组即可
if (j > jEnd) {
System.arraycopy(nums, i, arr, k, iEnd - i + 1);
}
}
public static void mergeInsertionSort(int[] a) {
int[] a2 = new int[a.length];
split(a, 0, a.length - 1, a2);
}
//归并排序分区方法
private static void split(int[] a, int left, int right, int[] a2) {
// 治:当分区范围 <= 32后,采用插入排序实现有序
// 传统的归并排序必须等到 left == right 即分区范围内只有一个元素时才视为有序
// 通过整合插入排序,可以对小范围内(<=32)的元素直接采用插入排序后即可视为有序
// 不用一直递归调用到分区内只有一个元素,提高效率和性能
if (right - left <= 32) {
//插入排序
insertionSort(a, left, right);
return;
}
// 分
int mid = (right - left) >>> 1;
split(a, left, mid, a2);
split(a, mid + 1, right, a2);
// 合
merge(a, left, mid, mid + 1, right, a2);
System.arraycopy(a2, left, a, left, right - left + 1);
}
/**
* 插入排序
*/
private static void insertionSort(int[] a, int left, int right) {
for (int low = left + 1; low <= right; low++) {
//定义一个变量t记录未排序区域的第一个值
int t = a[low];
//定义一个指针 i 为已排序区域的最后一个值的指针
int i = low - 1;
while (i >= left && a[i] > t) {
a[i + 1] = a[i];
i--;
}
//循环结束,说明找到了插入位置,插入即可
if (i != low - 1) {
a[i + 1] = t;
}
}
}
/**
* 合并有序数组
*
* @param a1 原始数组
* @param i iEnd 第一个有序范围
* @param j jEnd 第二个有序范围
* @param a2 临时数组
*/
private static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
//定义变量k为操作临时数组的指针
int k = i;
//如果i和j都在有效范围内
while (i <= iEnd && j <= jEnd) {
//比较i和j处索引的数组的值,并把较小的值加入到临时数组a2中
if (a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
//更新操作临时数组的指针
k++;
}
// 当i > iEnd说明第一个有序范围内的元素已经全部迭代,将第二范围内没有被迭代的元素拷贝到a1数组即可
if (i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd - j + 1);
}
// 当j > jEnd说明第二个有序范围内的元素已经全部迭代,将第一范围内没有被迭代的元素拷贝到a1数组即可
if (j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
六、快速排序
代码实现🎆
public void quickSort(int[] nums) {
doQuickSort(nums, 0, nums.length - 1);
}
private void doQuickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
// 分区
int p = partition(nums, left, right);
doQuickSort(nums, left, p - 1);
doQuickSort(nums, p + 1, right);
}
/**
* 单边循环快排(lomuto 洛穆托分区方案)
* 核心思想:每轮找到一个基准点元素,把比它小的放到它左边,比它大的放到它右边,这称为分区
*
* 1.选择最右元素作为基准点元素
* 2.j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
* · 交换时机:j 找到小的,且与 i 不相等
* · i 找到 >= 基准点元素后,不应自增
* 3.最后基准点与 i 交换,i 即为基准点最终索引
*
*/
private int partition(int[] nums, int left, int right) {
int pv = nums[right]; // 选择最右元素作为基准点元素
int i = left; // i找比基准点大的值,如果找到大于等于基准点的值,不再移动
int j = left; // j找比基准点小的值
while (j < right) {
if (nums[j] < pv) { // j找到小的了
if (j != i) {
swap(nums, i, j); // i和j不一样,则交换
}
// 只有找到的值比基准点小,i才++,如果大于基准点的值,i不变,j++
// 等到下次找到比基准点小的值,i和j不相等,进行交换
i++;
}
j++;
}
swap(nums, right, i);
return i;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
public void quickSort(int[] nums) {
doQuickSort(nums, 0, nums.length - 1);
}
private void doQuickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int p = partition(nums, left, right);
doQuickSort(nums, left, p - 1);
doQuickSort(nums, p + 1, right);
}
/**
* 双边循环快排
* 1.选择最左元素作为基准点元素
* 2.j 指针负责从右向左找比基准点小或等的元素,i 指针负责从左向右找比基准点大的元素,
一旦找到二者交换,直至 i,j 相交
* 3.最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置
*/
private int partition(int[] nums, int left, int right) {
/***************************随机元素作为基准点***************************/
int index = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(nums,index,left);
/***************************随机元素作为基准点***************************/
int pv = nums[left]; // 选择最左侧元素作为基准点
int i = left; // i 指针从左到右找大于基准点的值
int j = right; // j 指针从右到左找小于基准点的值
while (i < j) {
// 必须先处理j指针再处理i指针
// 1. j 从右向左找小(等)的
while (i < j && nums[j] > pv) {
j--;
}
// 2. i 从左向右找大的
while (i < j && nums[i] <= pv) {
i++;
}
// 3. 交换位置
swap(nums, i, j);
}
swap(nums, left, i);
return i;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
public void quickSort(int[] nums) {
doQuickSort(nums, 0, nums.length - 1);
}
private void doQuickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int p = partition(nums, left, right);
doQuickSort(nums, left, p - 1);
doQuickSort(nums, p + 1, right);
}
/*
核心思想是
* 改进前,i 只找大于的,j 会找小于等于的。一个不找等于、一个找等于,势必导致等于的值分布不平衡
* 改进后,二者都会找等于的交换,等于的值会平衡分布在基准点两边
*/
private int partition(int[] nums, int left, int right) {
// 随机基准点
int random = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(nums, left, random);
// 基准点
int pv = nums[left];
int i = left + 1; // 在处理重复值时,i要从left + 1开始
int j = right;
while (i <= j) {
// i 指针和 j 指针的先后处理方式不再重要
// 处理重复值,i需要从左到右找大于等于基准点的值
while (i <= j && nums[i] < pv) {
i++;
}
// 处理重复值,j需要从右向左找小于等于基准点的值
while (i <= j && nums[j] > pv) {
j--;
}
if (i <= j) {
swap(nums, i, j);
i++;
j--;
}
}
swap(nums, j, left);
return j;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}