一、插入排序
1、插入排序:
先看代码
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 && arr[j] > tmp;j--) {
arr[j+1] = arr[j];
}
arr[j+1] = tmp;
}
}
测试:
public static void main(String[] args) {
int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
System.out.println("排序前: " + Arrays.toString(arr1));
insertSort(arr1);
System.out.println("排序后: " + Arrays.toString(arr1));
}
结果:
思路:
1、循环嵌套,利用i和j双指针,i从1下标开始,到数组结束;
2、每次j循环能够保证的是i下标前面的元素是有序的。
时间复杂度: 最好情况下:O(n); 最坏情况下:O(n^2);
空间复杂度:O(1);
稳定性:插入排序是比较稳定的,它并没有跳跃式交换数值。
2、希尔排序
public static void shellSort(int[] arr) {
//将数组进行分组
int gap = arr.length-1;
while (gap > 1) {
//这里gap >= 1的话会死循环
//所以需要在循环结束后,数组趋于有序时进行最后的排序。
shell(arr,gap);
gap = (gap/3)+1;
}
shell(arr,1);
}
public static void shell(int[] arr,int gap) {
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
int j = i-gap;
for ( ;j >= 0 && arr[j] > tmp; j=j-gap) {
if (arr[j] > tmp) {
arr[j+gap] = arr[j];
}
}
arr[j+gap] = tmp;
}
}
测试:
public static void main(String[] args) {
int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
System.out.println("排序前: " + Arrays.toString(arr1));
shellSort(arr1);
System.out.println("排序后: " + Arrays.toString(arr1));
}
结果:
希尔排序是对插入的排序的优化,当数据量很大的时候,我们先将数据分成若干个组,然后进行类似于插入排序的排序。当gap=1的时候,数组已经趋近于有序,这个时候就会很快。整体而言,达到了优化的效果。
我说优化可能你也不信,现在我们来利用System.currentTimeMillis();
方法来获取当前时间(毫秒级别),然后用时间来粗略对比一下。
我们来测试一下当数据量很大的时候,用插入排序和希尔排序来对比下:
插入排序测试:
public static void main(String[] args) {
int[] arr = new int[10_0000];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(100000);
}
long Start = System.currentTimeMillis();
insertSort(arr);
long End = System.currentTimeMillis();
System.out.println("插入排序时间差: " + (End - Start));
}
插入排序测试结果(进行了三次测试):
1483 1780 1494
希尔排序测试:
public static void main(String[] args) {
int[] arr = new int[10_0000];
Random random = new Random();
for (int i = 0; i < arr.length; i++) {
arr[i] = random.nextInt(100000);
}
long Start = System.currentTimeMillis();
shellSort(arr);
long End = System.currentTimeMillis();
System.out.println("希尔排序时间差: " + (End - Start));
}
希尔排序测试结果(进行了三次测试):
17 16 19
不管你信不信,反正我信了!希尔排序做到了优化。
思路:
希尔排序的关键思路是和插入排序是一样的,但是需要将元素分组进行插入排序。
时间复杂度: 正常情况下:O(n^1.3-1.5); 最坏情况:O(n^2);
空间复杂度:O(1);
稳定性:不稳地,因为它拥有跳跃式交换数据。
二、选择排序
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
for (int j = i+1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
int tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
}
}
}
//另外一种写法
public static void selectSortII(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int max = 0;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[max]) {
max = j;
}
}
int tmp = arr[arr.length-i-1];
arr[arr.length-i-1] = arr[max];
arr[max] = tmp;
}
}
选择排序是一个比较简单的排序。我们来理理它的思路:
1、利用 i 循环遍历数组;
2、每次进入j循环后,j 循环做的事就是让 arr[i]
的值从 arr[i]
到 arr[arr.length-1]
的元素的值是最小的。
用更简单的话来说就是:(第二个写法更适合这种思路)
把数组分为有序区间和无序区间,每次从无序区间中找到最大的数放在无序区间的最后一位
测试:
public static void main(String[] args) {
int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
System.out.println("排序前: " + Arrays.toString(arr1));
selectSort(arr1);
System.out.println("排序后: " + Arrays.toString(arr1));
}
结果:
时间复杂度:O(n^2);
空间复杂度O(1);
稳定性:不稳地。
三、堆排序
我们先来说一下思路:
1、如果数组要升序,那么就是建立大堆;反之建立小堆;
2、每次将堆顶的元素与最后一个元素交换;交换后数组长度"减一";
3、在"减一"的这个数组中整体向下调整,保证堆顶的元素的最大的;
4、重复2-3步骤,直到"减一"到这个数组只有一个元素;
5、得到了升序数组。
又到了上代码环节:
public static void heapSort(int[] arr) {
//建大堆
creatHeap(arr);
//排序
int end = arr.length-1;
while (end > 0) {
int tmp = arr[end];
arr[end] = arr[0];
arr[0] = tmp;
//每次都向下调整,确保堆顶元素最大
adjustDown(arr,0,end);
//虽然我们传过去的时end,但是这个end代表的是数组的长度。
//所以不会调整到当前位置的元素。
end--;
}
}
//建大堆
public static void creatHeap(int[] arr) {
for (int parent = (arr.length-1-1)/2; parent >= 0; parent--) {
adjustDown(arr,parent,arr.length);
}
}
//向下调整
public static void adjustDown(int[] arr,int parent,int len) {
int child = 2*parent+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 = 2*parent+1;
} else {
break;
}
}
}
在堆排序的时候,你就把数组想成二叉树。虽然他没有left和right,毕竟堆就是用来存储完全二叉树的一种数据类型。
又到了测试环节:
public static void main(String[] args) {
int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
System.out.println("排序前: " + Arrays.toString(arr1));
heapSort(arr1);
System.out.println("排序后: " + Arrays.toString(arr1));
}
虽然这个数组已经是老演员了,但不影响它来测试我们的排序方法。
结果:
现在来解释一下上面说的数组长度"减一",因为每次end>0循环中都会将堆顶元素最大的值与堆尾元素进行交换,第一次拿下来的一定是整个数组中的最大值,当我们数组长度"减一"后,即不需要调整这个刚得到的最大值,又可以使用adjustDown函数使得堆顶元素变为"减一"数组后的最大。
时间复杂度: O(nlogn);
空间复杂度:O(1);
稳定性:不稳地。
四、冒泡排序
上代码!上代码!
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
boolean isSorted = true;//优化
for (int j = 0; j < arr.length-i-1; j++) {
if (arr[j] > arr[j+1]) {
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
isSorted = false;
}
}
if (isSorted) {
return;
}
}
}
测试:
public static void main(String[] args) {
int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
System.out.println("排序前: " + Arrays.toString(arr1));
bubbleSort(arr1);
System.out.println("排序后: " + Arrays.toString(arr1));
}
结果:
该看的都看了,现在我们进入正题,冒泡排序的思路:
1、定义i循环表示循环的次数;有几个元素就循环几次呗;
2、j循环中依次比较,从0到arr.length-i-1。都把大于的元素放在后面。
3、i的第一次循环一定能把最大的放在后面,第二次循环把第二大的放在倒数第二个位置,以此类推;最后数组有序。
来说说为啥j<arr.length-i-1
;因为第一次i循环已经保证最后一个元素有序了,那么第二次i循环j就可以不用管最后一个值了,而i代表的是上依次i循环中i有序数字的个数。至于这个-1,如果不加当i=1的时候可能会造成数组越界异常,因为有个arr[j] > arr[j+1]
;
从另外一方面理解,j+1已经可以读取到数组最后一个元素。所以一定要-1,记得!
关于优化,当某次进入i循环,j循环走完都没有进入if语句,那么证明数组是有序的,不用再进行比较了。直接return跑路。
时间复杂度(没优化的情况下) :O(n^2);
空间复杂度:O(1);
稳定性:稳定。
五、快速排序
现在终于来到了传说中的快排,我将用递归和非递归的两种代码来表达快排。
我们先来聊聊递归:分治的思想,把大问题化解成小问题。还记得求斐波那契数第n项吗?还记得让人欲仙欲死的二叉树吗?算了,不为难我自己了。
快排使用递归,其实就是:
1、找基准,让基准的左边小于基准,基准的右边大于基准,现在定义一个基准par。
2、递归基准的左边,(其实递归哪边无所谓的,你高兴你把你自己的左边递归了都可以)。左边low到 par-1;
3、递归基准的右边;右边par+1到high;
每次start和end基本上是不一样的;
4、终止条件:这也是递归的要素,当par左边或者右边只有一个元素或者没有元素时,说明它的左边或者右边有序了,此时下一个递归的start 或者end
它们存在着start>=end的关系。所以这就是递归的终止条件。
public static void quickSort(int[] arr) {
//quick(arr,0,arr.length-1);
quickImitate(arr,0,arr.length-1);
}
public static void quick(int[] arr,int low,int high) {
if (low >= high) {
return;
}
//优化二:使用insertSort优化 记得return
if ((high-low)+1 <= 100){
insertSort2(arr,low,high);
return;
}
int mid = (low + high)/2;
//优化一:三数取中 可以防止栈溢出情况 而栈溢出是因为数组原本有序
minOfThree(arr,low,mid,high);
int par = par(arr,low,high);
quick(arr,low,par-1);
quick(arr,par+1,high);
}
public static int par(int[] arr,int start,int end) {
int tmp = arr[start];
while (start < end) {
//让end往前走,找到一个比tmp还小的数字放在start位置
//end可能会走几步,所以写一个循环
while (start < end && arr[end] >= tmp) {//一定要写= 不然可能会陷入死循环
end--;
}
//此时end来到了比tmp还小的数字
//不小于tmp也可以赋值,此时start=end
if (arr[end] < tmp) {
arr[start] = arr[end];
}
//让start往前走,找到一个比tmp还小的数字让在end位置
//start可能会走几步,所以写一个循环
while (start < end && arr[start] <= tmp) {
start++;
}
//此时start来到了比tmp还大的数字
if (arr[start] > tmp) {
arr[end] = arr[start];
}
}
//此时end和start在同一个位置 将tmp的值放在这个位置上 此时start所在下标的左边比arr[start]小 start所在下标的右边比arr[start]大
arr[start] = tmp;//也可以换成arr[end] = tmp;
//此时start和end相遇的位置就是par
return start;
}
/**
* 优化一、三数取中
*/
public static void minOfThree(int[] arr,int low,int mid,int high) {
int x = arr[low];
int y = arr[mid];
int z = arr[high];
int[] tmp = {x,y,z};
for (int i = 1; i < tmp.length; i++) {
int n = tmp[i];
int j = i-1;
for ( ;j >= 0 && tmp[j] > n; j--) {
tmp[j+1] = tmp[j];
}
tmp[j+1] = n;
}
//会存在小问题 arr[mid]一定要放在中间赋值
arr[low] = tmp[1];
arr[mid] = tmp[0];
arr[high] = tmp[2];
}
/**
* 优化二:insertSort优化
*/
public static void insertSort2(int[] arr,int low,int high) {
for (int i = low; i <= high; i++) {
int tmp = arr[i];
int j = i-1;
for (; j >= 0 && arr[j] > arr[j+1]; j--) {
arr[j+1] = arr[j];
}
arr[j+1] = tmp;
}
}
测试:
public static void main(String[] args) {
int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
System.out.println("排序前: " + Arrays.toString(arr1));
quickSort(arr1);
System.out.println("排序后: " + Arrays.toString(arr1));
}
结果:
现在我们来说一下非递归:利用栈模拟
其实思路都一样,我们要做的就是:
1、找基准!
2、再次利用par函数得到par;
分别得到将start和par-1入栈,再将par+1和end入栈。
3、每次栈pop()两次;两个元素分别等于新的start和新的end传入到par函数中.
4、重复2-3步骤,直到栈为空的时候数组就有序了。
现在来实现代码:
public static void quickImitate(int[] arr,int low,int high) {
Stack<Integer> stack = new Stack<>();
//调用par函数的到第一次par
int par = par(arr,low,high);
//如果par左边有两个元素以上,那么入栈 先push end下标 再push start下标
if (par-1 > low) {
stack.push(par-1);
stack.push(low);
}
//如果par右边有两个元素以上,那么也入栈
if (par+1 < high) {
stack.push(high);//end
stack.push(par+1);
}
while (!stack.isEmpty()) {
int start = stack.pop();
int end = stack.pop();
int tmpPar = par(arr, start, end);
if (tmpPar - 1 > start) {
stack.push(tmpPar - 1);
stack.push(start);
}
//如果par右边有两个元素以上,那么也入栈
if (tmpPar + 1 < end) {
stack.push(end);//end
stack.push(tmpPar + 1);
}
}
}
这次就不请我们的老演员数组了,赶紧下一集归并排序。
时间复杂度:最好情况下O(nlogn);最坏情况O(n^2);
空间复杂度:最好情况:O(logn);最坏情况O(n);
稳定性:不稳地。
六、归并排序
还是一样的,归并排序分为递归和非递归。
递归解法:
public static void mergeSort(int[] arr) {
mergeRec(arr,0,arr.length-1);
}
public static void mergeRec(int[] arr,int start,int end) {
if (start >= end) {//此时只有一个元素
return;
}
int mid = (start+end)/2;
mergeRec(arr,start,mid);
mergeRec(arr,mid+1,end);
merge(arr,start,mid,end);
}
//相当于合并两个有序数组
public static void merge(int[] arr,int start,int mid,int end) {
int[] tmpArr = new int[end-start+1];//元素个数=下标相减+1
int s1 = start;
int e1 = mid;
int s2 = mid+1;
int e2 = end;
int k = 0;
while (s1 <= e1 && s2 <= e2) {
if (arr[s1] <= arr[s2]){
tmpArr[k] = arr[s1];
k++;
s1++;
}else {
tmpArr[k] = arr[s2];
k++;
s2++;
}
}
//这个时候有一个数组已经走完了
while (s1 <= e1) {
tmpArr[k] = arr[s1];
k++;
s1++;
}
while (s2 <= e2) {
tmpArr[k] = arr[s2];
k++;
s2++;
}
for (int i = 0; i < tmpArr.length; i++) {
arr[start+i] = tmpArr[i];
}
}
先来看张图:
从递归整体思路来解释归并排序:
递:将数组分解成"小数组";
归:将数组排序并归。
粗鲁点说,先让元素一个一个有序,再让元素两个两个有序,再让元素四个四个有序,一直到数组结束。
测试:
public static void main(String[] args) {
int[] arr1 = {12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
System.out.println("排序前: " + Arrays.toString(arr1));
quickSort(arr1);
System.out.println("排序后: " + Arrays.toString(arr1));
}
结果:
非递归实现:
public static void mergeSort1(int[] arr) {
for (int i = 1; i <= arr.length; i *= 2) {
merge1(arr,i);
}
}
public static void merge1(int[] arr,int gap) {
int[] tmpArr = new int[arr.length];
int s1 = 0;
int e1 = s1+gap-1;
int s2 = e1+1;
int e2 = s2+gap-1 >= arr.length ? arr.length-1 : s2+gap-1;
int k = 0;
while (s2 < arr.length) {
while (s1 <= e1 && s2 <= e2) {
if (arr[s1] <= arr[s2]) {
tmpArr[k++] = arr[s1++];
}else {
tmpArr[k++] = arr[s2++];
}
}
while (s1 <= e1) {
tmpArr[k++] = arr[s1++];
}
while (s2 <= e2) {
tmpArr[k++] = arr[s2++];
}
s1 = e2+1;
e1 = s1+gap-1;
s2 = e1+1;
e2 = s2+gap-1 >= arr.length ? arr.length-1 : s2+gap-1;
}
while (s1 < arr.length) {
tmpArr[k++] = arr[s1++];
}
for (int i = 0; i < tmpArr.length; i++) {
arr[i] = tmpArr[i];
}
}
思路:
1、将数组从前往后分组,第一次分为1组,第二次分为2组,第三次分为4组,第四次分为八组。
2、每次分组都能保证相邻俩个组之间的元素是有序的。
3、当两个分组的长度>=数组长度时,数组就有序了!
时间复杂度:O(nlogn);
空间复杂度:O(n);
稳定性:稳定。
七、简单总结
这些常见的排序建议多画图,跟着代码画图,多画图就能理解了。
老规矩,初学小白。如有错误,多多指正。感谢!