算法:排序算法
最优
有一句谚语:“All roads lead to Rome.” 面对一个编程问题时,我们有多种方法来实现;
但是怎样才能判断哪种方法才是最优的呢?
我们运行程序,当然希望程序越快,占内存越小;但是速度与空间,就像杠杆中的力臂与力,两者必须做出取舍。如果选择速度快,即时间复杂度小,程序所占内存空间的大小就会增加;而希望程序占内存小,程序运行的速度也会随之受影响。
当然,现如今大家考虑最优一般是从速度(时间复杂度)的角度出发。
时间频度
时间频度,一个算法中的语句执行的次数称为时间频度,记作T(n)
随着程序规模的变大,时间频度的常数项、低次项、系数可以忽略
例子
同样是计算100以内数的和,我们有2种实现方法
int total =0;
//1, for循环相加
int end=100;
for(int i=1;i<=end;i++){
total += i;
}
//2,利用等差数列公式
total =(1+end)*end /2;
第一种方法的时间频度:T(n) = n+1 //因为进入循环+判断
第二种方法的时间频度:T(n) = 1 //直接计算得出结果
时间复杂度
时间复杂度,也叫大O时间复杂度,表示方法是O(n)
由于程序规模的变大,时间频度有些地方可以忽略,从而产生了时间复杂度
例如,T(n) = n^2 +7n+6 ; T(n) = 3n^2 +2n+2
两者虽然时间频度不同,但是时间复杂度确实一样,都是O(n^2)
常见的时间复杂度 | 阶 | 例子 |
---|---|---|
常数阶 | O(1) | 12 |
对数阶 | O(logn) | 5log2n+20 |
平方阶(双层for循环) | O(n^2) | 3n^2+2n+1 |
线性阶 | O(n) | 2n+3 |
线性对数阶 | O(nlogn) | 2n+3nlog2n+19 |
立方阶(三层for循环) | O(n^3) | 6n3+2n2+3n+4 |
指数阶(要尽量避免) | O(2^n) | 2^n+3 |
k次方阶 | O(n^k) | n^k |
时间复杂度排序,由小变大:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(n^k) < O(2^n)
冒泡排序(Bubble Sorting)
用2个指针比较2个数,从左往右,如果第1个大于第2个,两个数交换位置
因为排序的数字是一个一个累加的,所以称之为冒泡排序
- 将指针从左往右遍历,需要一次for循环
- 至多遍历4次,才能达到最终的顺序,还需要for循环
第1次遍历完后,数组最后一个元素可以不用比较了, 所以for循环大小-1,也就是说:
第一次 ---- array.length -1
第二次 ---- array.length -2
第三次 ---- array.length -3
第四次 ---- array.length -4
所以:
package Sorting;
import java.util.Arrays;
public class BubbleSorting {
public static void main(String[] args) {
int[] array = {3, 9, -1, 10, -2};
int tmp =0;
for(int i=array.length-1; i>0; i--) {
for(int j=0;j<i;j++) {
if(array[j] > array[j+1]) {
tmp = array[j];
array[j] = array[j+1];
array[j+1] =tmp;
}
}
}
System.out.println(Arrays.toString(array));
}
}
冒泡排序优化
假设给出的数组原来就是有序的,或仅仅经过2次就正序了,我们可以直接跳出循环,中止程序
package Sorting;
import java.util.Arrays;
public class BubbleSorting {
public static void main(String[] args) {
// int[] array = {3, 9, -1, 10, -2};
int[] array = {3,2,6,4,5};
int tmp =0;
boolean flag=false; //flag判断是否进行了排序交换
for(int i=array.length-1; i>0; i--) {
for(int j=0;j< i;j++) {
if(array[j] > array[j+1]) {
tmp = array[j];
array[j] = array[j+1];
array[j+1] =tmp;
flag = true;
}
}
System.out.println("第"+(array.length-i)+"次遍历");
System.out.println(Arrays.toString(array));
if(!flag) {
break;
}else {
flag = false; //用于判断下一次有没有交换,如果没有,表示数据已正序
}
}
}
}
第1次遍历在交换顺序,第2次遍历判断是否正序
因为有2个嵌套式的循环,冒泡排序的时间复杂度是n^2
选择排序(Select Sorting)
选择排序和冒泡排序很像,但是两者还是有所区别
在选择排序中,我们要选择最小的数据,并把最小的数据放到整个数组最前面
当我们遍历数组查找最小元素时,我们仅仅记下最小元素下标,只有当前数组遍历结束时,数组才会发生交换,效率相比于冒泡排序(每次遍历都要相互交换)大大提高。
int[] array = {101,34,119,1};
//第一轮将最小的移到最前面 1, 34, 119, 101
//第二轮将剩下的最小的移到前面 1, 34, 119, 101
//第三轮将剩下的最小的移到前面 1,34,101,119
从上我们可以看出,每次循环遍历后,初始位置都会向后挪1位(因为最小的已经在前面了)
我们依次比较剩下的数,所以需要array.length-1次才能将数从小到大依次移到前面
package Sorting;
import java.util.Arrays;
public class SelectionSorting {
public static void main(String[] args) {
int[] array = {101,34,119,1};
for(int i=0;i<array.length-1;i++) {
int smallest=i;
for(int j=i;j<array.length;j++) {
if(array[j]< array[smallest]) {
smallest = j;//我们仅仅记下最小元素下标
}
}
//结束遍历后才能发生交换
if(i!=smallest) {
int temp = array[i];
array[i] = array[smallest];
array[smallest] = temp;
}
System.out.println("第"+(i+1)+"轮循环:"+Arrays.toString(array));
}
}
}
插入排序(Insert Sorting)
第1轮:我们从第2个数开始,把第2个数和前面的数进行比较,从右往左,如果第2个数小于第1个数,两者交换位置
第2轮:我们从第3个数开始,把第3个数和前面的数进行比较(0,1),如果第3个数小于第2个数,两者交换,如果交换后,第1个数比交换后的第2个数大,继续交换,直至到最后
…
第n轮:从第n+1个数开始,把第n+1个数和前面的数(n个数)进行比较,找个合适的地方插入(这就是插入排序)
一共有array.length-1轮
package Sorting;
import java.util.Arrays;
public class InsertSorting {
public static void main(String[] args) {
int[] array = { 93, 54, 77, 31, 44, 226, 55 };
int temp;
//换位法
for (int i = 1; i < array.length; i++) {
for (int j = i; j > 0; j--) {
if (array[j] < array[j - 1]) {
temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}else {
break;
}
}
//移位法
// temp = array[i];
// int j =i;
// while(j>0 && temp<array[j-1]) {
// array[j] = array[j - 1];
// j--;
// }
//
// if (j != i) {
// array[j] = temp;
// }
System.out.println("第" + i + "轮:" + Arrays.toString(array));
}
}
}
希尔排序(Shell Sorting)
希尔排序是由希尔所发现的,所以命名为Shell Sorting. 希尔排序本质上还是插入排序Insert Sorting, 但是希尔排序运行效率相比上面大大提升。
第一轮:
排序前要先给数据分组,让数组的长度/2,即array.length /2, 得出的是组中数据的间隔。我们在分好的组内进行插入排序,间距此时是array.length 除以2。第1组插入排序进行完后,移入第2组,依次进行,直到最后一组。
第二轮:
总体和上面一样,但是每组数据的间隔是之前的一半,(array.length/2)/2
…
直到最后一轮,步数为1,普通的插入排序,但是遍历次数减少
package Sorting;
import java.util.Arrays;
public class ShellSorting {
public static void main(String[] args) {
int[] array = {59,48,75,107,86,23,37,59,65,14 };
int length = array.length;
int temp;
//步长
for(int step = length/2;step >0; step /=2) {
//插入算法循环次数
for(int i=step;i<length;i++) {
//控制插入算法的比较和交换
//移位法
temp = array[i];
int j =i;
if(array[j] < array[j-step]) {
while(j>=step && temp<array[j-step]) {
array[j] = array[j - step];
j-=step;
}
}
array[j] = temp;
/**
* 换位法
for(int j=i;j>=step;j-=step) {
if(array[j]<array[j-step]) {
temp = array[j];
array[j] = array[j-step];
array[j-step] = temp;
}
else
break;
}
*/
}
System.out.println(Arrays.toString(array));
}
}
}
快速排序(Quick Sorting)
快速排序是一种非常经典的排序算法,它的运行效率非常高,当然快排属于空间换时间的一类,因为要使用递归。
基本思路:
选取数组最中间的数为基数(以这个数为基准),用2个指针分别从最左边和最右边读取数据,将小于基数的数放在左边,大于基数的数放在右边。最后排完,小于的都在左侧,大的都在右侧。我们继续进行递归,由于此时左侧和右侧的指针都指向基数,为了分别进行左排序递归和右排序递归,左侧指针右移1格,右指针左移1格。
图解:
(哎,图片不清晰)
package Sorting;
import java.util.Arrays;
public class QuickSorting2 {
public static void main(String[] args) {
// int[] array = {59,48,75,107,86,23,37,59,65,14 };
int[] array = {77,26,93,17,54,31,44,55,20 };
QuickSort(array, 0, array.length-1);
System.out.println(Arrays.toString(array));
}
public static void QuickSort(int[] array, int left, int right) {
int l =left;
int r =right;
int temp;
int pivot = array[(left+right)/2];
while(l<r) {
//左边往右走
while(array[l]<pivot) {
l+=1;
}
//右边往左走
while(array[r]>pivot) {
r -=1;
}
if(l>=r) {
break;
}
temp =array[l];
array[l]=array[r];
array[r] = temp;
//交换后发现array[l] == pivot的值 r--;前移
if(array[l] == pivot) {
r -=1;
}
//交换后发现array[r] == pivot的值 l++;前移
if(array[r] == pivot) {
l +=1;
}
}
//递归
if(left<r) {
QuickSort(array, left, r-1);
}
if(right>l) {
QuickSort(array, l+1, right);
}
}
}
以第一个数为基准的快速排序
package Sorting;
import java.util.Arrays;
public class QuickSorting3 {
public static void main(String[] args) {
int[] array = {77,26,93,17,54,31,44,55,20 };
Sort(array, 0, array.length-1);
System.out.println(Arrays.toString(array));
}
public static void Sort(int[] array, int left, int right) {
int l = left;
int r = right;
int pivot = array[left]; //以第一个数为基准
while(true) {
while(array[r] >= pivot&&l<r) {
r-=1;
}
if(l<r) {
array[l] =array[r];
l+=1;
}
while(array[l] <= pivot&&l<r) {
l +=1;
}
if(l<r) {
array[r] = array[l];
r -=1;
}
if(l==r) {
array[l] = pivot;
break;
}
}
if(left<r) {
Sort(array, left, r-1);
}
if(right > l) {
Sort(array, l+1, right);
}
}
}
归并排序(Merge Sorting)
归并排序和上面的快速排序思想上类似,都用到了经典的分治算法(divide and conquer),即先将复杂的问题划分divide成小的问题然后递归求解,然后再conquer(把各个划分阶段的答案修补在一起)
Divide
Conquer
为了方便操作,我们传进去的数组会被复制,输出的是一个新的数组,这个数组就是排序好的数组
Divide分的时候要采用递归,每次分2份,直到数组每个元素都被分别分在1个数组中
当递归完后,回溯时从最后一个数组开始,即一个一个元素的数组,返回上一层,将此时的数组赋给left或right,接着调用Conquer,把2个数组排序放入一个新的数组,然后再返回,依次进行,直到全部排序完成
package Sorting;
import java.util.Arrays;
public class MergeSorting {
public static void main(String[] args) {
// int[] array = {59,48,75,107,86,23,37,59,65,14 };
int[] array = {77,26,93,17,54,31,44,55,20 };
MergeSort(array);
System.out.println(Arrays.toString(array));
}
public static void MergeSort(int[] arr) {
int[] arr2 = Divide(arr);
for(int i=0;i<arr2.length;i++) {
arr[i]=arr2[i];
}
}
public static int[] Divide(int[] arr) {
int pivot = (arr.length)/2;
if(pivot>=1) {
int[] left = new int[pivot];
int[] right = new int[arr.length-pivot];
for(int i=0;i<left.length;i++) {
left[i] = arr[i];
}
for(int j=0;j<right.length;j++) {
right[j]=arr[pivot+j];
}
// System.out.println(Arrays.toString(left));
// System.out.println(Arrays.toString(right));
// System.out.println("---");
left = Divide(left);
right = Divide(right);
return Conquer(left, right);
}
return arr;
}
public static int[] Conquer(int[] arr1, int[] arr2) {
int[] temp = new int[arr1.length+arr2.length];
int cur =0;
int left =0;
int right =0;
while(true) {
if(arr1[left]<arr2[right]) {
temp[cur] = arr1[left];
cur++;
left++;
}
else if(arr1[left] > arr2[right]) {
temp[cur] = arr2[right];
cur++;
right++;
}
else {
temp[cur] =arr1[left];
cur++;
left++;
temp[cur] = arr2[right];
cur++;
right++;
}
if(left>=arr1.length) {
for(int i=right;i<arr2.length;i++) {
temp[cur] = arr2[i];
cur++;
}
break;
}
else if(right>=arr2.length){
for(int i=left;i<arr1.length;i++) {
temp[cur] = arr1[i];
cur++;
}
break;
}
}
return temp;
}
}
基数排序(Radix Sorting)
基数排序是特殊的桶排序
步骤:
- 先将数组按个位排序分类,分好后一个一个放入原数组
- 再把数组按照十位排序分类,同上
- 直到分到原数组中最大数的位数,例如最大数是3789,我们最后就要按照千位来排序
由小到大,这样依次我们就完成对数组的排序
package Sorting;
import java.util.Arrays;
public class RedixSorting {
public static void main(String[] args) {
int[] array = {77,26,93,17,54,107,31,44,55,20 };
RadixSort(array);
System.out.println(Arrays.toString(array));
}
public static void RadixSort(int[] array) {
//找出最大的一位
int Max =0;
for(int i=0;i<array.length;i++) {
if(array[Max] <array[i]) {
Max =i;
}
}
//得出最大一位的个数
int count=0;
int temp =array[Max];
while(temp>0) {
temp /= 10;
count++;
}
for(int j=0;j<count;j++) {
busket(array, pow(10,j));
}
}
//计算一个数的几次方
public static int pow(int num, int exp) {
if(exp==0) {
return 1;
}
for(int i=0;i<exp-1;i++) {
num *= num;
}
return num;
}
public static void busket(int[] array, int digit) {
//创建一个桶用来记录数据
int[][] bucket = new int[10][array.length];
//创建一个数组来记录每个桶里面元素的个数
int[] itemNum = new int[10];
for(int i=0;i<array.length;i++) {
int no = array[i]/digit%10; //哪一个桶
bucket[no][itemNum[no]] = array[i] +10;
itemNum[no] +=1;
}
int index=0;
for(int j=0;j<10;j++) {
for(int k=0;k<bucket.length;k++) {
if(bucket[j][k] !=0) {
array[index] = bucket[j][k]-10;
bucket[j][k] =0;
index++;
}
else {
break;
}
}
itemNum[j] =0;
}
index =0;
}
}
排序算法的时间复杂度比较
排序法 | 平均时间 | 最差情况 |
---|---|---|
冒泡 | O(n^2) | O(n^2) |
交换 | O(n^2) | O(n^2) |
选择 | O(n^2) | O(n^2) |
插入 | O(n^2) | O(n^2) |
基数 | O(logR B) | O(logR B) |
Shell | O(nlogn) | O(n^s) 1<s<2 |
快排 | O(nlogn) | O(n^2) |
归并 | O(nlogn) | O(nlogn) |
堆 | O(nlogn) | O(nlogn) |
术语:
稳定:两个数据在排序后任然保持之前的相对前后位置
时间复杂度:算法运行所消耗的时间
测试程序
package Sorting;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TestSortingTime {
public static void main(String[] args) {
int[] array = new int[80000];
Init(array);
System.out.println("BubbleSorting");
PrintTime("Start: ");
BubbleSorting(array);
PrintTime("End: ");
System.out.println();
Init(array);
System.out.println("SelectionSorting");
PrintTime("Start: ");
SelectionSorting(array);
PrintTime("End: ");
System.out.println();
Init(array);
System.out.println("InsertSorting");
PrintTime("Start: ");
InsertSorting(array);
PrintTime("End: ");
System.out.println();
Init(array);
System.out.println("ShellSorting");
PrintTime("Start: ");
ShellSorting(array);
PrintTime("End: ");
System.out.println();
Init(array);
System.out.println("QuickSorting");
PrintTime("Start: ");
QuickSort(array, 0, array.length-1);
PrintTime("End: ");
System.out.println();
Init(array);
System.out.println("QuickSorting2");
PrintTime("Start: ");
QuickSort2(array, 0, array.length-1);
PrintTime("End: ");
System.out.println();
Init(array);
System.out.println("MergeSorting");
PrintTime("Start: ");
MergeSort(array);
PrintTime("End: ");
System.out.println();
Init(array);
System.out.println("RedixSorting");
PrintTime("Start: ");
RadixSort(array);
PrintTime("End: ");
}
public static void Init(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = (int) (Math.random() * 80000);
}
}
public static void PrintTime(String words) {
Date date1 = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
String time = sdf.format(date1);
System.out.println(words + time);
}
public static void BubbleSorting(int[] array) {
int tmp = 0;
boolean flag = false; // flag判断是否进行了排序交换
for (int i = array.length - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (array[j] > array[j + 1]) {
tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
flag = true;
}
}
if (!flag) {
break;
} else {
flag = false; // 用于判断下一次有没有交换,如果没有,表示数据已正序
}
}
}
public static void SelectionSorting(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
int smallest = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[smallest]) {
smallest = j;
}
}
if (i != smallest) {
int temp = array[i];
array[i] = array[smallest];
array[smallest] = temp;
}
}
}
public static void InsertSorting(int[] array) {
int temp;
for (int i = 1; i < array.length; i++) {
for (int j = i; j > 0; j--) {
if (array[j] < array[j - 1]) {
temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}else {
break;
}
}
}
}
public static void ShellSorting(int[] array) {
int length = array.length;
int temp;
for(int step = length/2;step >0; step /=2) {
for(int i=step;i<length;i++) {
temp = array[i];
int j =i;
if(array[j] < array[j-step]) {
while(j>=step && temp<array[j-step]) {
array[j] = array[j - step];
j-=step;
}
}
if (j != i) {
array[j] = temp;
}
// for(int j=i;j>=step;j-=step) {
// if(array[j]<array[j-step]) {
// temp = array[j];
// array[j] = array[j-step];
// array[j-step] = temp;
// }
// else
// break;
// }
}
}
}
public static void QuickSort(int[] array, int left, int right) {
int l =left;
int r =right;
int temp;
int pivot = array[(left+right)/2];
while(l<r) {
//左边往右走
while(array[l]<pivot) {
l+=1;
}
//右边往左走
while(array[r]>pivot) {
r -=1;
}
if(l>=r) {
break;
}
temp =array[l];
array[l]=array[r];
array[r] = temp;
//不进行下面的操作程序会卡死
//交换后发现array[l] == pivot的值 r--;前移
if(array[l] == pivot) {
r -=1;
}
//交换后发现array[r] == pivot的值 l++;前移
if(array[r] == pivot) {
l +=1;
}
}
if(left<r) {
QuickSort(array, left, r-1);
}
if(right>l) {
QuickSort(array, l+1, right);
}
}
public static void QuickSort2(int[] array, int left, int right) {
int l = left;
int r = right;
int pivot = array[left];
while(true) {
while(array[r] >= pivot&&l<r) {
r-=1;
}
if(l<r) {
array[l] =array[r];
l+=1;
}
while(array[l] <= pivot&&l<r) {
l +=1;
}
if(l<r) {
array[r] = array[l];
r -=1;
}
if(l==r) {
array[l] = pivot;
break;
}
}
if(left<r) {
QuickSort2(array, left, r-1);
}
if(right > l) {
QuickSort2(array, l+1, right);
}
}
//归并排序
public static void MergeSort(int[] arr) {
int[] arr2 = Divide(arr);
for(int i=0;i<arr2.length;i++) {
arr[i]=arr2[i];
}
}
public static int[] Divide(int[] arr) {
int pivot = (arr.length)/2;
if(pivot>=1) {
int[] left = new int[pivot];
int[] right = new int[arr.length-pivot];
for(int i=0;i<left.length;i++) {
left[i] = arr[i];
}
for(int j=0;j<right.length;j++) {
right[j]=arr[pivot+j];
}
left = Divide(left);
right = Divide(right);
return Conquer(left, right);
}
return arr;
}
public static int[] Conquer(int[] arr1, int[] arr2) {
int[] temp = new int[arr1.length+arr2.length];
int cur =0;
int left =0;
int right =0;
while(true) {
if(arr1[left]<arr2[right]) {
temp[cur] = arr1[left];
cur++;
left++;
}
else if(arr1[left] > arr2[right]) {
temp[cur] = arr2[right];
cur++;
right++;
}
else {
temp[cur] =arr1[left];
cur++;
left++;
temp[cur] = arr2[right];
cur++;
right++;
}
if(left>=arr1.length) {
for(int i=right;i<arr2.length;i++) {
temp[cur] = arr2[i];
cur++;
}
break;
}
else if(right>=arr2.length){
for(int i=left;i<arr1.length;i++) {
temp[cur] = arr1[i];
cur++;
}
break;
}
}
return temp;
}
public static void RadixSort(int[] array) {
//找出最大的一位
int Max =0;
for(int i=0;i<array.length;i++) {
if(array[Max] <array[i]) {
Max =i;
}
}
//得出最大一位的个数
int count=0;
int temp =array[Max];
while(temp>0) {
temp /= 10;
count++;
}
for(int j=0;j<count;j++) {
busket(array, pow(10,j));
}
}
//计算一个数的几次方
public static int pow(int num, int exp) {
if(exp==0) {
return 1;
}
for(int i=0;i<exp-1;i++) {
num *= num;
}
return num;
}
public static void busket(int[] array, int digit) {
//创建一个桶用来记录数据
int[][] bucket = new int[10][array.length];
//创建一个数组来记录每个桶里面元素的个数
int[] itemNum = new int[10];
for(int i=0;i<array.length;i++) {
int no = array[i]/digit%10; //哪一个桶
bucket[no][itemNum[no]] = array[i] +10;
itemNum[no] +=1;
}
int index=0;
for(int j=0;j<10;j++) {
for(int k=0;k<bucket.length;k++) {
if(bucket[j][k] !=0) {
array[index] = bucket[j][k] -10;
bucket[j][k] =0;
index++;
}
else {
break;
}
}
itemNum[j] =0;
}
index =0;
}
}
运行结果
参考资料
尚硅谷数据结构与算法:https://www.bilibili.com/video/BV1E4411H73v?p=56&spm_id_from=pageDriver
菜鸟教程:https://www.runoob.com/wp-content/uploads/2019/03/0B319B38-B70E-4118-B897-74EFA7E368F9.png