一、插入排序
直接插入排序-原理:
整个区间被分为:
- 有序区间
- 无序区间
每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入
public static void insertSort(long[] array) {
// 一共要取多少个元素来进行插入过程(无序区间里有多少个元素)
for (int i = 0; i < array.length - 1; i++) {
// 有序区间 [0, i] 至少在 i == 0 的时候得有一个元素
// 无序区间 [i + 1, n)
// 先取出无序区间的第一个元素,记为 k
long k = array[i + 1];
// 从后往前,遍历有序区间{
// 找到合适的位置退出
// 所谓合适的位置,就是第一次 k >= array[j] 的位置
int j;
for (j = i; j >= 0 && k < array[j]; j--) {
array[j + 1] = array[j]; // 将不符合条件的数据往后般一格
}
array[j + 1] = k;
}
}
时间复杂度:
最好情况是 O(n)
平均/最坏情况是 O(n^2)
空间复杂度: O(1)
稳定性: 具备稳定性
二、希尔排序
希尔排序-原理:
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
- 希尔排序是对直接插入排序的优化。
- 当gap>1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。
// 希尔排序
public static void shellSort(long[] array) {
int gap = array.length / 2;
while (gap != 1) {
insertSortWithGap(array, gap);
gap = gap / 2;
}
// 最后再执行一次插排
insertSort(array);
}
// gap: 间隔 / 一共多少组
private static void insertSortWithGap(long[] array, int gap) {
// 外围的循环次数是 n - gap 次
for (int i = 0; i < array.length - gap; i++) {
// 一共有 gap 个分组
// 认为一开始 [0, i + gap) 有序
// [i + gap, n) 无序
long k = array[i + gap];
int j;
// j 下标只去找同一组的元素比较,所以每次跳过 gap
for (j = i; j >= 0 && k < array[j]; j = j - gap) {
array[j + gap] = array[j];
}
array[j + gap] = k;
}
}
时间复杂度:
最好情况是:O(n)
平均情况是:O(n^1.3)
最坏情况是:O(n^2)
空间复杂度: O(1)
稳定性: 不具备稳定性
三、选择排序
直接选择排序-原理:
每一次从无序区间选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素排完。
升序
public static void selectSort1(long[] array) {
// 每次选择出最大的数,放到最后去
// 一共要选择出 n - 1 个数
for (int i = 0; i < array.length - 1; i++) {
// 通过遍历无序区间,只需要找到最大的数最在的位置就行(以下标的形式体现)不要做交换
// 无序 [0, n - i)
int maxIndex = 0; // 假设最大的数放在一开始
for (int j = 1; j < array.length - i; j++) {
if (array[j] > array[maxIndex]) {
// 说明,找到了无序区间的新的最大的数
// 所以,记录最大数的下标
maxIndex = j;
}
}
// 遍历完成之后,无序区间的最大的数就放在 maxIndex 所在下标处
// array[maxIndex] 是最大数
// 交换 [maxIndex] 和 无序区间的最后一个元素 [n - i - 1]
swap(array, maxIndex, array.length - i - 1);
}
}
降序
public static void selectSort2(long[] array) {
// 每次选择出最小的数,放到最后去
// 一共要选择出 n - 1 个数
for (int i = 0; i < array.length - 1; i++) {
int minIndex = 0;
for (int j = 1; j < array.length - i; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
swap(array, minIndex, array.length - 1 - i);
}
}
时间复杂度::O(n^2)
空间复杂度: O(1)
稳定性: 不具备稳定性
四、堆排序
堆排序-原理:
基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
注意: 排升序要建大堆;排降序要建小堆。
public static void heapSort(long[] array) {
// 1. 建立大堆
createBigHeap(array);
// 2. 遍历 n - 1 次
for (int i = 0; i < array.length - 1; i++) {
// 2.1 交换之前的无序区间 [0, n - i)
swap(array, 0, array.length - i - 1);
// 交换之后的无序区间 [0, n - i - 1),元素个数 n - i - 1 个
// 2.1 对堆的 [0] 进行向下调整,堆里的元素个数就是无序区间的元素个数
shiftDown(array, array.length - i - 1, 0);
}
}
private static void createBigHeap(long[] array) {
for (int i = (array.length - 2) / 2; i >= 0; i--) {
shiftDown(array, array.length, i);
}
}
private static void shiftDown(long[] array, int size, int index) {
while (2 * index + 1 < size) {
int maxIndex = 2 * index + 1;
int right = maxIndex + 1;
if (right < size && array[right] > array[maxIndex]) {
maxIndex = right;
}
if (array[index] >= array[maxIndex]) {
return;
}
swap(array, index, maxIndex);
index = maxIndex;
}
}
private static void swap(long[] array, int i, int j) {
long t = array[i];
array[i] = array[j];
array[j] = t;
}
时间复杂度: O(n * log(n))
空间复杂度: O(1)
稳定性: 不具备稳定性
五、冒泡排序
冒泡排序-原理:
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
public static void bubbleSort(long[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean sorted = true;
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
sorted = false;
swap(array, j, j + 1);
}
}
if (sorted) {
return;
}
}
}
时间复杂度:
最好情况是 O(n)
平均/最坏情况是 O(n^2)
空间复杂度: O(1)
稳定性: 具备稳定性
六、快速排序
快速排序-原理:
- 从待排序区间选择一个数,作为基准值(pivot);
- Partition:遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
- 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度==0,代表没有数据。
public static void quickSort(long[] array) {
quickSortRange(array, 0, array.length - 1);
}
// 为了代码书写方便,我们选择使用左闭右闭的区间表示形式
// 让我们对 array 中的从 from 到 to 的位置进行排序,其他地方不用管
// 其中,from,to 下标的元素都算在区间的元素中
// 左闭右闭的情况下,区间内的元素个数 = to - from + 1;
private static void quickSortRange(long[] array, int from, int to) {
if (to - from < 1) {
// 区间中元素个数 <= 1 个
return;
}
// 挑选中区间最右边的元素 array[to]
// array[to] 还是 array[to - 1] 还是 array[array.length] 还是 array[array.length - 1] 呢?
int pi = partitionMethodA(array, from, to);
// 小于等于 pivot 的元素所在的区间如何表示 array, from, pi - 1
// 大于等于 pivot 的元素所在的区间如何表示 array, pi + 1, to
// 按照分治算法的思路,使用相同的方式,处理相同性质的问题,只是问题的规模在变小
quickSortRange(array, from, pi - 1); // 针对小于等于 pivot 的区间做处理
quickSortRange(array, pi + 1, to); // 针对大于等于 pivot 的区间做处理
}
/**
* 以区间最右边的元素 array[to] 最为 pivot,遍历整个区间,从 from 到 to,移动必要的元素
* 进行分区
* @param array
* @param from
* @param to
* @return 最终 pivot 所在的下标
*/
private static int partitionMethodA(long[] array, int from, int to) {
// 1. 先把 pivot 找出来
long pivot = array[to];
// 2. 通过定义 left 和 right 两个下标,将区间划分出来
int left = from;
int right = to;
// [from, left) 都是 <= pivot 的
// [left, right) 都是未参与比较的
// [right, to] 都是 >= pivot 的
// 循环,保证每个元素都参与了和 pivot 的比较
// 也就是,只要 [left, right) 区间内还有元素,循环就应该继续
while (left < right) {
// while (right - left > 0) {
// 先让左边进行比较
// 随着 left 在循环过程中一直在 left++,请问 left < right 的条件能一定保证么
// 不一定,所以,我们时刻进行 left < right 条件的保证
// 并且,只有在 left < right 成立的情况下,array[left] 和 pivot 的比较才有意义
// left < right && array[left] <= pivot 的顺序不能交换
while (left < right && array[left] <= pivot) {
left++;
}
// 循环停止时,说明 array[left] > pivot
while (left < right && array[right] >= pivot) {
right--;
}
// 循环停止时,说明 array[right] < pivot
// 两边都卡住时,交换 [left] 和 [right] 位置的元素
long t = array[left];
array[left] = array[right];
array[right] = t;
}
// 说明 left == right,说明 [left, right) 区间内一个元素都没有了
// 所有元素都和 pivot 进行过比较了,然后都在各自应该的位置上了
// 并且 array[left] 一定是 >= pivot 的第一个元素(不给大家证明了)
long t = array[to];
array[to] = array[left];
array[left] = t;
// 返回 pivot 最终所在下标
return left;
}
private static int partitionMethodB(long[] array, int from, int to) {
long pivot = array[to];
int left = from;
int right = to;
while (left < right) {
while (left < right && array[left] <= pivot) {
left++;
}
array[right] = array[left];
while (left < right && array[right] >= pivot) {
right--;
}
array[left] = array[right];
}
array[left] = pivot;
return left;
}
时间复杂度:
最好/平均情况是 O(n * log(n))
最坏情况是 O(n^2)
空间复杂度:
最好/平均情况是 O(log(n))
最坏情况是 O(n)
稳定性: 不具备稳定性
七、归并排序
归并排序-原理:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
public static void mergeSort(long[] array) {
mergeSortRange(array, 0, array.length);
}
// 这里的 [from, to) 是左闭右开来表示区间的
private static void mergeSortRange(long[] array, int from, int to) {
int size = to - from;
// 情况1:如果区间内元素个数 <= 1 个,什么都不需要做
if (size <= 1) {
return;
}
// 其他情况
// 1. 找到区间的中间位置的下标
int mid = from + (size / 2);
// 2. 优先对左 [from, mid) 和右 [mid, to) 两个小区间先进行排序(使用同样的方式处理:分治思想)
mergeSortRange(array, from, mid);
mergeSortRange(array, mid, to);
// 3. 有了两个分别各自有序的小区间 [from, mid) 和 [mid, to)
// 通过一定的方式,将 [from, mid) [mid, to) 合并到 [from, to) 都是有序的
// 两个有序数组区间合并到一个有序数组区间(需要结果放回原地去)
merge(array, from, mid, to);
}
private static void merge(long[] array, int from, int mid, int to) {
// 先计算出来额外空间需要多个,计算两个区间加起来多少个元素
int size = to - from;
// 申请一个额外的数组作为临时保存的地方
long[] other = new long[size];
int left = from; // 左边小区间的下标
int right = mid; // 右边小区间的下标
int dest = 0; // 临时空间的下标
// 只要左右两个小区间还有元素要参与比较
while (left < mid && right < to) {
if (array[left] <= array[right]) {
other[dest++] = array[left++];
} else {
other[dest++] = array[right++];
}
}
// 其中一个区间的元素取完了,另一个区间里一定还有元素,再把剩余的元素,统统放入 other
// 看起来左右两个都写了,但执行起来的时候一定只有一个会执行
while (left < mid) {
other[dest++] = array[left++];
}
while (right < to) {
other[dest++] = array[right++];
}
// 把 other 中的有序元素,复制回 array 中,要注意下标的问题
for (int i = 0; i < size; i++) {
array[from + i] = other[i]; // array 下标的基准是从 [from] 开始,other 下标的基准是从 [0] 开始
// offset(偏移)是一致的,都是 i
}
}
时间复杂度: O(n * log(n))
空间复杂度: O(n)
稳定性: 具备稳定性