视频参考:B站:马士兵,青岛大学–王卓。
时间复杂度 Big O
算法花费时间随着问题规模的扩大的变化
计算执行次数最多语句的时间随着规模扩大的规律,然后忽略常数项、低次项
不考虑必须要做的操作:循环、赋初值、程序初始化…;
不考虑常数项;
不考虑低次项;
一般时间复杂度都是“最差”的情况
E.g.:
访问数组某个位置的值: O(1)
访问链表某个位置的值: O(n)
求数组平均数:O(n)
选择排序 SelectionSort
- 找到最小的数的位置(索引)
- 把这个位置上的数和【0】上的数交换
- 把除第一个位置之后的剩余部分的数组重复过程,确定位置【1】的数
- 循环3的操作,直到最后一个数
笔记:外循环i控制每轮要排的最小值的索引;内循环遍历最小值索引后的每一位索引的值,与当前最小值索引上的值作比较,把最小的值换到最小值索引上。
/**
* 选择排序
* @param arr
*/
public static void selectionSort(int[] arr) {
//由于要保证i后面还有数才需要排序,所以i最大可以取到[arr.length-2]就行
for (int i = 0; i < arr.length-1 ; i++) { //执行n次
int minPos = i; //每次循环的要求的最小值的位置都加1 //执行n次
for (int j = i+1; j < arr.length ; j++) { //执行 (n-1)+(n-2)+...+1 次 = n(n-1)/2
if(arr[j]<arr[minPos]){
//如果后面的值小,交换位置
swap(arr,minPos,j);
}
}
}
}
/**
* 交换数组中两个不同位置元素的值
* @param arr
* @param i
* @param j
*/
static void swap(int[] arr, int i ,int j){
int temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
/**
* 打印数组
* @param arr
*/
static void print(int[] arr){
for (int i : arr) {
System.out.print(i+"\t");
}
}
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
稳定性:不稳定,排序后相等值的相对位置可能会发生变化。
冒泡排序 BubbleSort
内循环每轮排好一个最大数
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length-1 ; i++) { //要排arr.length-1轮
for (int j = 0; j < arr.length-1-i ; j++) { //-i的原因是 在新的一轮,需要比较的数比上一轮少一个
if(arr[j]>arr[j+1]) swap(arr,j,j+1); //排好一个最大数
}
}
}
每趟不断两两比较,并按规则前小后大交换。
每一趟能够排好一个最大数。
时间复杂度分析:
最好:数组原本有序,比较 n-1 次,移动0次
最坏:数组逆序,比较n(n-1)/2次,移动3n(n-1)/2次
时间复杂度:最差
O
(
n
2
)
O(n^2)
O(n2),最好
O
(
n
)
O(n)
O(n),平均
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:O(1)
稳定性:稳定
插入排序 InsertionSort
类似整理扑克牌,外循环抽出第i张牌(i之前的牌已有序),内循环用这张牌与前面的每个牌作比较,如果比前面一张小,就交换位置,直到检查至第一张牌。
在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
在插入a[i]之前,前半段序列 a[0]~a[i-1] 是有序的,所以a[i]需要在找到有序位置j(0<=j<=i)
插入a[i]时,如果前一个数比a[i]大,那么就与a[i]交换位置,相当于a[i]前进一位,直到a[i]大于等于它前面的数,则排好了a[i]。
//外层循环控制要排的第i个数
for (int i = 1; i < arr.length; i++) {
for (int j = i; j > 0; j--) {
if(arr[j]<arr[j-1]){
Utils.swap(arr,j,j-1);
}
}
}
另一种做法:使用临时变量x存储要排的a[i],如果a[i-1]比x大,那就向后一位,覆盖a[i]。。。以此循环,直到某个数比临时变量x小,那么将x赋值到这个数的后面位置。
public static void sort(int[] arr){
for (int i = 1; i < arr.length; i++) {
int x = arr[i];
//System.out.println("x:"+x);
for (int j = i-1; j >= 0 ; j--) {
//System.out.println("arr[j]:"+arr[j]);
if(arr[j]>x){
arr[j+1] = arr[j];
//System.out.println(Arrays.toString(arr));
if(j==0) arr[j] = x;
}else {
arr[j+1] = x;
//System.out.println("结束内循环---"+Arrays.toString(arr));
break;
}
}
}
}
时间复杂度分析:
最好的情况:数组全部是有序的,每个元素只要比较一次,移动0次。所以要比较n-1次(第一个元素不需要比较)
最坏的情况:数组逆序有序,第二个元素需要和前面的1个元素比较,第三个需要和前面2个比较,第四个需要和前面3个比较…第n个需要和前面(n-1)个元素比较,累计起来要比较 (n)(n-1)/2 次,移动次数大约也是n(n-1)/2次
时间复杂度,最好情况是 O ( n ) O(n) O(n),最坏情况是 O ( n 2 ) O(n^2) O(n2),平均也是 O ( n 2 ) O(n^2) O(n2)。
空间复杂度: O ( 1 ) O(1) O(1),最多只需要一个额外变量,和有多少个元素排序没有关系。
稳定性:稳定
简单排序算法总结
冒泡:基本不用,太慢
选择:基本不用,不稳
插入:样本小且基本有序的时候效率比较高
希尔排序
改进的插入排序
对固定间隔的数 组成的数组进行插入排序,并不断缩小间隔。
以间隔为5为例子:
package sortingAlgorithm.ShellSort;
import sortingAlgorithm.Utils.Utils;
import java.util.Arrays;
public class ShellSortDemo {
public static void main(String[] args) {
int[] arr = {81,94,11,96,12,35,17,95,28,58,41,75,15};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] arr){
//假设间隔为5
int h = 5;
//对数组进行间隔为5的排序, 外循环控制要排的那组数的最后一个数
//注意j的范围一定要大于4,防止数组越界,因为j-5必须大于等于0
for (int i = 5; i < arr.length ; i++) {
for (int j = i; j > 4 ; j-=5) {
if(arr[j]<arr[j-5]){
Utils.swap(arr,j,j-5);
}
}
}
}
}
加入外循环 :以Knuth序列公式 不断缩小间隔大小,进行排序。
public static void sort(int[] arr){
//Knuth序列找出最佳间隔
int h = 1;
while(h <= arr.length / 3){
h = h*3 + 1;
}
//不断缩小间隔,直到gap=1
for(int gap = h; gap > 0; gap = (gap-1)/3){
//使用当前间隔数,对所有数前面对应间隔的数作比较
for (int i = gap; i < arr.length; i++) {
//插入排序间隔组成的新数组
for(int j = i; j > gap-1 ; j-=gap){
if(arr[j]<arr[j-gap]){
Utils.swap(arr,j,j-gap);
}
}
}
}
}
时间复杂度:与排序的元素个数和使用的序列有关,约 O(n^(1.25))~O(1.6*n^(1.25))
空间复杂度:O(1)
稳定性:不稳定,在进行某次间隔排序时,相等的值的相对位置可能发生改变。
归并排序 MergeSort
使用递归,把原数组一分为二,对左数组排序,对右数组排序,再排序合并两个有序数组。
过程是递归的,不断一分为二,直到左右两个数组只有一个数时,开始返回。
归并排序是稳定的,所以java等语言里对于对象的排序使用的是归并排序。
public class MergeSort {
public static void main(String[] args) {
int[] arr = {1,4,5,8,3,6,9};
sort(arr,0,arr.length-1);
Arrays.stream(arr).forEach(x -> {System.out.print(x+" ");});
}
/* //排序合并两个有序数组的过程
public static void merge(int[] arr){
int[] temp = new int[arr.length]; //定义用于存放合并后数组的空数组
int i = 0; //第一个有序数组的第一个索引
int j = arr.length/2+1; //第二个有序数组的第一个索引
int k = 0; //合并数组的第一个索引
//当i,j还未到达各自部分的尽头时
while(i<=arr.length/2 && j< arr.length){
//如果索引i对应的值小于等于索引j的值,就把该值放入temp数组当前位置,放完后k索引加1,索引i加1
if(arr[i]<=arr[j]){
temp[k] = arr[i];
i++;
k++;
}else {
temp[k] = arr[j];
j++;
k++;
}
}
//如果其中一个有序数组还有未到达尽头的部分,直接加到temp数组后面
while (i<=arr.length/2) temp[k++] = arr[i++];
while (j<arr.length) temp[k++] = arr[j++];
}*/
//改造方法,变得更灵活
//加入参数:左指针,右指针,右边界
public static void merge(int[] arr,int leftPtr,int rightPtr,int rightBound){
int mid = rightPtr - 1;
int[] temp = new int[rightBound - leftPtr +1];
int i = leftPtr;
int j = rightPtr;
int k = 0;
while(i<=mid && j< arr.length){
temp[k++] = arr[i]<=arr[j] ? arr[i++] : arr[j++];
}
while (i<=mid) temp[k++] = arr[i++];
while (j<=rightBound) temp[k++] = arr[j++];
for (int m=0; m<temp.length;m++) arr[leftPtr+m] = temp[m];
}
//左边界, 右边界
public static void sort(int[] arr,int left,int right){
if(left == right) return;
//分成两半
int mid = (left+right)/2;
//左边排序
sort(arr,left,mid);
//右边排序
sort(arr,mid+1,right);
//合并有序数组
merge(arr,left,mid+1,right);
}
}
快速排序 QuickSort
pivot: 轴,中心点。
基本思想:
任取一个元素为中心(pivot)(这里代码实现使用了最后一个值为轴)
所有比中心小的元素放在中心左边,比中心元素大的放在中心元素右边。
形成左右两个子表。
再对子表重新选择中心元素,并以此规则划分。
直到每个子表的元素只剩一个。(递归的结束条件)
public class QuickSort {
//对数组的指定范围进行排序
public static void sort(int[] arr,int leftBound,int rightBound){
if(leftBound >= rightBound) return;
int mid = partition(arr,leftBound,rightBound); //分成左右两部分,得到轴的位置
//对左右两个部分分别排序
sort(arr,leftBound,mid-1);
sort(arr,mid+1,rightBound);
}
//以数组最后一个值为轴,将小于等于该值的元素移到轴的左边,大于该值的元素移到轴的右边
public static int partition(int[] arr,int leftBound,int rightBound){
int pivlot = arr[rightBound]; //轴
int left = leftBound; //左指针
int right = rightBound-1; //右指针
System.out.println("Initial array:"+Arrays.toString(arr));
//使用两个指针,不断移动,交换左指针上大于等于轴的值和右指针上小于等于轴的值
while(left <= right){
//找到从左指针开始第一个大于等于轴的数
while (left<=right && arr[left]<=pivlot) left++;
//找到从右指针开始第一个小于轴的数
while (left<=right && arr[right]>pivlot) right--;
//交换这两个数的位置
if(left<right) {
//System.out.println("Find two Ptrs to swap: left:"+left+" right:"+right);
Utils.swap(arr,left,right);
//System.out.println("After swap:"+ Arrays.toString(arr));
}
}
//将轴的值与中间位置的值交换,即左指针的当前位置,
//因为左指针此时指向的是右子序列的第一个元素,它必然大于轴
//这样就形成了 左子序列 轴 右子序列 的排列组合
Utils.swap(arr,left,rightBound);
//返回轴的位置索引
return left;
}
public static void main(String[] args) {
int[] arr = {49,37,65,97,76,13,27,48};
System.out.println(partition(arr, 0, arr.length - 1));
}
}
平均时间复杂度:
O
(
n
l
o
g
n
)
O(nlog^n)
O(nlogn)
空间复杂度:需要
O
(
l
o
g
n
)
O(log^n)
O(logn)的栈空间
稳定性:不稳定,在进行某次划分时,相等的值的相对位置可能发生改变。
计数排序 CountSort
适合 量大但是范围小
如何快速得知高考名次
入门demo:
public class CountSortDemo {
public static void main(String[] args) {
int[] arr = {2,4,2,3,7,1,1,0,0,5,6,9,8,5,7,4,0,9};
int[] result = sort(arr);
System.out.println(Arrays.toString(result));
}
public static int[] sort(int[] arr){
//定义结果数组
int[] result = new int[arr.length];
//定义计数数组,长度为原数组中元素的种类
int[] count = new int[10];
//遍历原数组,将原数组的元素,按照: 索引:原数组元素值 值:出现的次数 放入计数数组中
for (int i = 0; i < arr.length; i++) {
count[arr[i]]++;
}
//遍历计数数组,将计数数组的内容,按顺序放入结果数组
for(int i=0,j=0; i < count.length ; i++){
while(count[i]>0){
result[j] = i;
j++;
count[i]--;
}
}
return result;
}
}
改进版(稳定):
public class CountSortReview {
public static void main(String[] args) {
int[] arr = {4,5,3,2,1,0};
int[] result = sort(arr);
System.out.println(Arrays.toString(result));
}
public static int[] sort(int[] arr){
int k = 6;//元素的种类
int[] result = new int[arr.length];
int[] count = new int[k];
// 计数数组: 索引:原数组元素值 值:该元素的个数
for (int i = 0; i < arr.length; i++) {
count[arr[i]]++;
}
//System.out.println("1st count:"+Arrays.toString(count));
// 改造该数组为增量数组, 每个索引指向的值代表小于等于该索引值的数的个数
// 索引指向的值代表该索引值在排序好的数组中最后出现的位置
for (int i = 1; i < count.length; i++) {
count[i] = count[i] + count[i-1];
}
//System.out.println("2nd count:"+Arrays.toString(count));
//反向遍历原数组
//每次往向结果数组中放值时,先减去count数组该值对应的数量,-1则代表该值在结果数组里的索引
// do not forget index j can equal to 0
for (int j = arr.length-1;j>=0;j--){
result[--count[arr[j]]] = arr[j];
//System.out.println(Arrays.toString(result));
}
return result;
}
}
基数排序 RadixSort
package sortingAlgorithm.RadixSort;
import java.util.Arrays;
public class RadixSort {
public static void main(String[] args) {
int[] arr = {123,456,325,678,523,256,721,850};
int[] result = sort(arr);
System.out.println(Arrays.toString(result));
}
public static int[] sort(int[] arr){
int d = findMaxCount(arr);//种类个数(排数字时就是最长数字的位数)
//System.out.println("d:"+d);
//计数数组,这里是数字,所以有0~9,共10个桶
int[] count = new int[10];
//结果数组
int[] result = new int[arr.length];
//如果有k位,那么就有k次 分配+组合 的过程
for(int i = 0;i < d;i++){
int divid = (int) Math.pow(10,i);
//用于得到位上的数:
//求百位上的数,就是用该数除以100再%10,求十位上的数就是该数除以10再%10,求个位上的就是除以1再%10
// System.out.println(divid);
//遍历原数组,填充计数数组: 索引:0~9的数字 值:该数字出现的次数
for (int j = 0; j < arr.length; j++) {
int num = arr[j]/divid % 10; //得到该位上的数
count[num]++;
}
//System.out.println("1st count:"+ Arrays.toString(count));
//改变计数数组到增量数组,类似计数排序
for (int k = 1; k < count.length; k++){
count[k] = count[k] + count[k-1];
}
//System.out.println("2nd count:"+ Arrays.toString(count));
//反向遍历原数组,放置值到结果数组中
//与计数排序不同的是 count数组的索引不再是原数组的值,而是原数组的值的某一位的值
for(int n = arr.length-1;n>=0;n--){
int num = arr[n]/divid % 10;
result[--count[num]] = arr[n];
}
//将该轮按某位排好的result数组复制到原数组上,完成该轮的组合
//下一轮将使用排序过的arr再按某位进行排序
System.arraycopy(result,0,arr,0,arr.length);
//将count数组归零,用于下一轮某位的排序
Arrays.fill(count,0);
}
return result;
}
public static int findMaxCount(int[] arr){
int max = arr[0];
for (int i : arr) {
if(i>max) max=i;
}
int count = 0;
while(max>0){
max/=10;
count++;
}
return count;
}
}