目录
前言:
🎈排序的算法有很多,对于数据量和数据所存在的特点不同,排序算法的使用场景也会相应的改变,这样才会增加我们程序的效率。
插入排序
🧢插入排序的算法思想,类似于我们打扑克,揭牌时整牌时的行为。我们首先认为一个数据是有序的,当我们拿到第二个数据,和前面数据去比较(这里排升序),如果比它小,把前面的数据就往后面放,直到找到前面数据比它小的时候,就放在这个数据的后面。
🧢我们每拿到一个数据,它前面的数据都是有序的,因此我们采取这样的思路当遍历完数组后,整组数据全都会有序。
代码实现
import java.util.Arrays;
public class InsertSort {
public static void insertSort(int[] arr) {
for(int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j = i - 1;
for(; j >= 0; j--) {
if(tmp < arr[j]) {
arr[j + 1] = arr[j];
}else {
//arr[j + 1] = tmp;
break;
}
}
arr[j + 1] = tmp;
}
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1,0};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
}
时间,空间复杂度分析
最坏情况:O(N^2)
😯当一组数据是逆序的时候,这样每次拿到一组数据都要一直往前比,放在最前面。
最好情况:O(N)
😯当一组数据是升序的时候,我们只需遍历一遍数组,不用挪动数据。
空间复杂度:O(1)
😯没有开辟临时的空间。
稳定性:稳定
注意:时间复杂度最坏达到O(N^2),但当数据有序的时候达到O(N),对比就可得到,当一组数据接近有序时(数据量不多),插入排序是非常快的。
希尔排序
😊希尔排序是对插入排序的一个升级,由于插入排序是拿到数据后一个一个比较,最终才能确定好位置。如果一个特别小的数据在最后面,那么把他往前放,就需挪动很多的数据。
😊希尔排序采取分组的思想,看上图,我们把1和7,5和4,6和3,各分为一组。然后把每组的数据进行插入排序,这样特别小的数据如果在最后面,很快就会放在前面去。这叫做预排序,每次排完后数据都会接近有序(到数据有序时,可能会分很多次组)。最终gap会为1,这个时候经历了预排序,数据接近有序,插入排序就会非常快。
😊分组的函数现在有很多说法,我们采取gap = gap / 2。
代码实现
import java.util.Arrays;
public class ShellSort {
private static void insertSort(int[] arr, int gap) {
for(int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j = i - gap;
for( ; j >= 0; j -= gap) {
if(tmp < arr[j]) {
arr[j + gap] = arr[j];
}else {
//arr[j + 1] = tmp;
break;
}
}
arr[j + gap] = tmp;
}
}
public static void shellSort(int[] arr) {
int gap = arr.length;
while(gap > 0) {
gap /= 2;
insertSort(arr, gap);
}
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1,0};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
}
时间,空间复杂度分析
时间复杂度:O(N^1.3)
😃我们每次按照最坏的情况算,会算出一个值,但是每次预排序数据都会接近有序,这样算显然不对。直到现在也没有算出一个确切的值,我们就先记住这个值。
空间复杂度:O(1)
😃没有额外开辟空间
稳定性:不稳定
堆排序
🎉堆排序的前提是我们首先要将这组数据建堆,建堆以及堆的概念,对于堆的一些操作在下面这篇博客中有详细介绍。
(141条消息) 模拟实现优先级队列_小小太空人w的博客-CSDN博客https://blog.csdn.net/weixin_62353436/article/details/127301135🪖我们要排升序,就要建大堆。大堆最大的数据在一组数据的最前面,我们将这个数据换到最后面。由于数据原本就是大堆的结构,换完之后,只需要将第一个数据向下调整(建大堆),由于最后一个数据位置已经确定,这次向下调整,就不用包含最后一个数据,这样就会又确定一个最大的数据在最前面。依次循环下去,我们就逐渐确定好每一个数据的位置,整组数据也就有序了。
代码实现
import java.util.Arrays;
public class HeapSort {
//向下调整时间复杂度:log(n)
private static void shiftDown(int[] arr, int parent, int len) {
int child = (parent * 2) + 1;
while(child < len) {
if(child + 1 < len && arr[child] < arr[child + 1]) {
child++;
}
if(arr[parent] < arr[child]) {
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
parent = child;
child = parent * 2 + 1;
}else {
break;
}
}
}
/**
* 时间复杂度:
* O(n) + O(n*logn) 约等于 O(nlogn)
* 空间复杂度:O(1)
* @param arr
*/
public static void heapSort(int[] arr) {
//建堆
//时间复杂度:O(n) 错位相减法推导
for(int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(arr, i, arr.length);
}
//交换元素,建堆
//时间复杂度:n * log(n)
int end = arr.length - 1;
while(end > 0) {
int tmp = arr[end];
arr[end] = arr[0];
arr[0] = tmp;
shiftDown(arr, 0, end);
end--;
}
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
}
时间,空间复杂度分析
时间复杂度:O(N * log(N))
🎄向下调整,调整的是数的高度log(n),我们从那个父亲节点(上面那个博客有介绍),向前依次向下调整(建堆)。堆的结构是一棵树,我们可以计算出每个节点向下调整的次数,然后求和,根据错位相减法,可求得建堆的时间复杂度为O(N)。然后将每个最大的数据依次放在最后面,向下调整,时间复杂度为:O(N * log(N))。总的时间复杂度为:O(N * log(N)) + O(N)约等于O(N * log(N))。
空间复杂度:O(1)
🎄没有额外开辟空间。
稳定性:不稳定
快速排序
挖坑法(图以右边做为key)
😆首先把最左面的数据取出来作为key,这时候坑就在最左面。然后从数据的右面开始找比key小的数据,放入坑位。这个时候坑位就在右面那个比key小的数据那里。然后从左面找比key大的数据,放入新的坑位。依次循环,直到左右指针相遇,key放入这里。这样走一遍就可以保证key的左面数据比它小,右面比它大,即这个数据位置就确定了,这个位置称为基准。
😆然后分别递归这个数据的左右区间,当递归到剩余一个数据时,认为有序。递归时每次都可以确定一个数据的位置,当左右区间递归完成后,整组数据就有序了。
代码实现
🤨这里会讲解三种方法找基准,它们递归的思路都是一致的。这里先展示找基准的代码,整体代码在后面展示。
private static int partition(int[] arr, int left, int right) {
int pivot = arr[left];
while(left < right) {
while(left < right && arr[right] >= pivot) {
right--;
}
arr[left] = arr[right];
while(left < right && arr[left] <= pivot) {
left++;
}
arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}
Hoare法(左右指针法)
💎找最左面作为key,然后从最右面开始找比key小的数据,从左面找比key大的数据。找到后两个数据交换。直到左右指针相遇,交换最左面和相遇位置的数据。最终,key的左面比它小,右面比它大,基准就可以确定。
注意:如果认为最左面数据为key,则必须先从右边开始找比它小的数据,这样才能保证最终key的左面比它小,右面比它大。右面和左面找小和大的数据时,必须取等号,防止左右两边数据一样时,会死循环。
代码实现
private static int partitionHoare(int[] arr, int left, int right) {
int tmp = left;
int pivot = arr[left];
while(left < right) {
//必须取等号,防止左右两端数据一致,死循环
//左面取key,必须右面先走,才能保证左面比key小,右面比key大
while(left < right && arr[right] >= pivot) {
right--;
}
while(left < right && arr[left] <= pivot) {
left++;
}
swap(arr, right, left);
}
swap(arr, left, tmp);
return left;
}
前后指针法
😉定义prev在起始位置,cur在其后面的位置,最左面的数据作为key。cur往前走找比key小的数据,找到后,prev往前走,交换两个位置的数据。如果cur找到比key小的数据时,prev往前走,两个指针指向同一个数据,就没有必要交换了。当cur遍历完数组后,把key放在prev的位置。这时候key的左面比它小,右面比它大,就可以确定基准。
代码实现
private static int partition2(int[] arr, int left, int right) {
int prev = left;
int cur = left + 1;
while(cur <= right) {
if(arr[cur] < arr[left] && arr[++prev] != arr[cur]) {
swap(arr, prev, cur);
}
cur++;
}
swap(arr, prev, left);
return prev;
}
时间,空间复杂度分析
时间复杂度:O(N * log(N))
😯递归就是一棵树,它每层都有N个数据,有log(N层)。
空间复杂度:O(log(N))
😯递归是要为函数开辟栈帧的,有log(N)层。
稳定性:不稳定
快速排序优化
🎈如果一组数据是逆序的,将最左面数据作为key。从右面找比它小的数据,左面没有数据,确定基准后左面也没有数据。这样时间复杂度会很大,就是一个等差数列,达到O(N^2),空间复杂度也会达到O(N)(类似一个单链表)。
🎈我们可以换一个key,找完基准后,保证它的左右都有数据。采取三位取中法,最左面和中间和最右面找中间大的数据,然后将他换到最左面。这样就算是逆序的数据,找完基准后,可以保证它的左右区间都有数据。
🎈快速排序,采用的是递归的思想。递归就怕递归太深了,导致栈溢出(StackOverFlow)。我们可以想到递归太深,大量数据都是在下面,而且它们也都是接近有序的,这个时候采取直接插入排序就会快很多,也可以防止继续向下递归。
整体代码实现
import java.util.Arrays;
public class QuickSort {
private static void insertSort(int[] arr, int left, int right) {
for(int i = left + 1; i <= right; i++) {
int tmp = arr[i];
int j = i - 1;
for( ; j >= left; j--) {
if(tmp < arr[j]) {
arr[j + 1] = arr[j];
}else {
//arr[j + 1] = tmp;
break;
}
}
arr[j + 1] = tmp;
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* Hoare法,左右指针法
* 快速排序:找基准 确定一个数的位置,左边比它小,右边比它大
* 时间复杂度:O(n*logn) 每层都是n个数据,有log(n)层
* 空间复杂度:O(logn) 递归树的高度
* @param arr
* @param left
* @param right
* @return
*/
private static int partitionHoare(int[] arr, int left, int right) {
int tmp = left;
int pivot = arr[left];
while(left < right) {
//必须取等号,防止左右两端数据一致,死循环
//左面取key,必须右面先走,才能保证左面比key小,右面比key大
while(left < right && arr[right] >= pivot) {
right--;
}
while(left < right && arr[left] <= pivot) {
left++;
}
swap(arr, right, left);
}
swap(arr, left, tmp);
return left;
}
/**
* 挖坑法
* 先将左面作为key存起来,即坑位。右面找小的数据,往坑位放,即右面为坑位,左面找大的数据往坑位放
* @param arr
* @param left
* @param right
* @return
*/
private static int partition(int[] arr, int left, int right) {
int pivot = arr[left];
while(left < right) {
while(left < right && arr[right] >= pivot) {
right--;
}
arr[left] = arr[right];
while(left < right && arr[left] <= pivot) {
left++;
}
arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}
/**
* 前后指针法
* cur找比key小的数据,往后面放,当prev和cur在同一位置时,就不用放了,最后将prev和key换
* @param arr
* @return
*/
private static int partition2(int[] arr, int left, int right) {
int prev = left;
int cur = left + 1;
while(cur <= right) {
if(arr[cur] < arr[left] && arr[++prev] != arr[cur]) {
swap(arr, prev, cur);
}
cur++;
}
swap(arr, prev, left);
return prev;
}
private static int findMidValOfIndex(int[] arr, int begin, int end) {
int mid = (begin + end) >> 1;
if(arr[begin] < arr[end]) {
if(arr[mid] > arr[end]) {
return end;
}else if(arr[mid] < arr[begin]) {
return begin;
}else {
return mid;
}
}else {
if(arr[mid] > arr[begin]) {
return begin;
}else if(arr[mid] < arr[end]){
return end;
}
return mid;
}
}
private static void _quickSort(int[] arr, int begin, int end) {
//剩余一个数,认为有序,递归结束条件
if(begin >= end) {
return;
}
//递归到后面数据量占总数的绝大多数,也接近有序,直接采用插入排序
if(end - begin + 1 <= 15) {
insertSort(arr, begin, end);
return;
}
//三位取中,防止最坏情况(数据有序)没有左区间,
//保证关键子在中间,左右都有区间
//空间复杂度达到O(n) 时间复杂度达到O(n^2)
int index = findMidValOfIndex(arr, begin, end);
swap(arr, index, begin);
int pivot = partition2(arr, begin, end);
//分别去递归这个基准的左右区间
_quickSort(arr, begin, pivot - 1);
_quickSort(arr, pivot + 1, end);
}
public static void quickSort(int[] arr) {
_quickSort(arr, 0, arr.length - 1);
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1,0};
quickSort(arr);
System.out.println(Arrays.toString(arr));
}
}
快速排序非递归
🤞利用栈,用区间控制模拟递归的一个过程。首先将一组数据的左右区间入栈,然后栈不为空的话,弹出这个区间,去找基准。找到后分别将这个基准的左右区间入栈,然后再去判断栈不为空,这个时候弹出的区间就是这个基准的右区间,依次循环下去,直到栈为空。如果区间剩余一个数据,就不用入栈了,认为它是有序的。
代码实现
public static void quickSort(int[] arr) {
Stack<Integer> stack = new Stack<>();
int begin = 0;
int end = arr.length - 1;
if(end > begin) {
stack.push(begin);
stack.push(end);
}
while(!stack.isEmpty()) {
end = stack.pop();
begin = stack.pop();
int pivot = findPivot(arr, begin, end);
if(pivot - 1 > begin) {
stack.push(begin);
stack.push(pivot - 1);
}
if(end > pivot + 1) {
stack.push(pivot + 1);
stack.push(end);
}
}
}
归并排序
😃归并思想,如果两组数据,都是有序的。开辟一个可以存储两组数据的空间,然后同时遍历这两组数据,进行比较。把小的数据往开辟的数组里放。然后遍历这个数组的指针往后走,继续比较,也是放小的数据。最终肯定有一个数组先遍历完成,然后将没有遍历完数组中的数据拷贝到新开辟的数组内。
😃那么一组数据的左右区间无序怎么办?采取递归的思想,当递归到剩余一个数据时认为数据是有序的,然后进行归并。最终会是一个一个,两个两个,四个四个等等有序的,当递归回退到整组数据的区间时,归并后,整组数据就会有序了。
😃最后需将有序的这个数组拷贝到原来的数组,由于是分区间递归下去的,那么当归并到一组数据的右区间有序时,也要将这组有序数据拷贝到这个右区间。
代码实现
import java.util.Arrays;
public class MargeSort {
private static void marge(int[] arr, int begin, int mid, int end) {
int len = end - begin + 1;
int[] tmp = new int[len];
int index = 0;
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
while(begin1 <= end1 && begin2 <= end2) {
if(arr[begin1] <= arr[begin2]) {
tmp[index++] = arr[begin1++];
}else {
tmp[index++] = arr[begin2++];
}
}
while(begin1 <= end1) {
tmp[index++] = arr[begin1++];
}
while(begin2 <= end2) {
tmp[index++] = arr[begin2++];
}
for(int i = 0; i < index; i++) {
arr[i + begin] = tmp[i];
}
}
private static void margeSortChild(int[] arr, int begin, int end) {
if(begin == end) {
return;
}
int mid = (begin + end) >> 1;
//递归左右区间,到一个数据,认为有序
margeSortChild(arr, begin, mid);
margeSortChild(arr, mid + 1, end);
//合并
marge(arr, begin, mid, end);
}
public static void margeSort(int[] arr) {
margeSortChild(arr, 0, arr.length - 1);
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1,0};
margeSort(arr);
System.out.println(Arrays.toString(arr));
}
}
时间,空间复杂度分析
时间复杂度:O(N * log(N))
🪖递归的思想,每层都有N个数据,有log(N)层。
空间复杂度:O(N)
🪖最终会开辟一个有N个数据的数组。
稳定性:稳定
归并排序非递归
😄我们利用gap控制区间,实现每组数据的归并。首先将gap设置为1,一个,一个数据归并。gap以二倍的速度增长,如果有10个数据,那么最后一次归并gap就是8,即gap需小于数组的长度。在进行归并的时候,每次归并的是两组数据。归并完成后,遍历数组的下标需向后跳两倍的gap。
😄当我们拿到一组数据的区间,和中间位置就可以归并,如果有10个数据,当gap为8时,确定右区间时end就有可能越界,这时就需修正它到数组的最后一个位置。如果有1个数据,确定中间位置也会越界,也需修正它到数组的最后一个位置。
代码实现
import java.util.Arrays;
public class MargeSort2 {
private static void marge(int[] arr, int left, int mid, int right) {
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int[] tmp = new int[right - left + 1];
int index = 0;
while(begin1 <= end1 && begin2 <= end2) {
if(arr[begin1] <= arr[begin2]) {
tmp[index++] = arr[begin1++];
}else {
tmp[index++] = arr[begin2++];
}
}
while(begin1 <= end1) {
tmp[index++] = arr[begin1++];
}
while(begin2 <= end2) {
tmp[index++] = arr[begin2++];
}
for(int i = 0; i < index; i++) {
arr[i + left] = tmp[i];
}
}
/**
* 一个一个有序,两个两个有序,四个四个有序
* @param arr
*/
public static void margeSort(int[] arr) {
int gap = 1;
while(gap < arr.length) {
//一组内有两个gap
for(int i = 0; i < arr.length; i += gap * 2) {
int left = i;
int mid = left + gap - 1;
int right = mid + gap;
//可能越界,修正到最后面
if(mid > arr.length - 1) {
mid = arr.length - 1;
}
if(right > arr.length - 1) {
right = arr.length - 1;
}
marge(arr, left, mid, right);
}
gap *= 2;
}
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1,0};
margeSort(arr);
System.out.println(Arrays.toString(arr));
}
}
计数排序
🙂找出一组数据中最小和最大的数据,然后开辟一个这个区间大小的数组。遍历数组,把每个数据减去最小值,作为下标,让计数数组加1。当遍历完成后,就可以统计出每个数据的个数。再去遍历开辟的这个数组,当计数数组不为0时,用这个下标加上最小值,依次存储到原来的数组。计数数组是几就存储几次,这样最终整组数据都会有序。
代码实现
import java.util.Arrays;
public class CountSort {
public static void countSort(int[] arr) {
int index = 0;
int min = arr[0];
int max = arr[0];
for(int i = 1; i < arr.length; i++) {
if(arr[i] > max) {
max = arr[i];
}
if(arr[i] < min) {
min = arr[i];
}
}
int[] tmp = new int[max - min + 1];
for(int i = 0; i < arr.length; i++) {
tmp[arr[i] - min]++;
}
//O(n + 范围)
for(int i = 0; i < tmp.length; i++) {
while(tmp[i] != 0) {
arr[index++] = i + min;
tmp[i]--;
}
}
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1,0};
countSort(arr);
System.out.println(Arrays.toString(arr));
}
}
时间,空间复杂度分析
时间复杂度:O(N + 范围)
😐第一次遍历数组O(N),第二次遍历数组O(N),第三次遍历开辟的数组O(n + 范围),计数排序适合对范围性数据排序。
空间复杂度:O(范围)
😐开辟了一个范围大小的数组。
稳定性:不稳定
冒泡排序
😣第一个和第二个数据比较,大的数据往后交换,然后两两比较,当遍历完数组后,最大的数据就会冒到最后面。再去冒0 - n-1这个区间的数据,把最大的数据冒到n-1这个位置。依次循环,每冒一个数据,就确定一个数据,也少比较一个数据。
代码实现
import java.util.Arrays;
public class BubbleSort {
public static void bubbleSort(int[] arr) {
for(int i = 0; i < arr.length - 1; i++) {
//优化操作,数据本来就有序
boolean flag = false;
for(int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j + 1]) {
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = true;
}
if(!flag) {
break;
}
}
}
}
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1,0};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
冒泡排序优化
😯如果第一次遍历完数组后,没有挪动数据,说明数组就是有序的,直接break跳出循环。
时间,空间复杂度分析
时间复杂度:O(N^2)
🤞每次冒一个数据到后面,这就是一个等差数列,时间复杂度为O(N^2)。考虑优化的最好时间复杂度:O(N),只遍历一遍数组。
空间复杂度:O(1)
🤞没有开辟新的数组。
稳定性:稳定
小结:
🐵排序算法代码的实现,有很多细节性的东西,我们要理解算法的思路,考虑全面,然后再去写代码。