时间复杂度与常见的排序算法
一、时间复杂度
1.1 常数时间的操作
- 一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
什么是常数操作????
//将数组上i位置上的数直接赋给a,这就是常数操作
int a=arr[i];
//将链表list, i位置上的上数取出来,这个过程经历了遍历,无论是从左遍历还是从右遍历,取到i的这个过程,就不能称为常数操作;
int b=list.get(i);
//数组一块连续的存储区域,而链表是线性结构,无法通过位置上的对比去取值,而只能一个个遍历
//同时,+ - * / ^运算也都是常数操作
总结:
跟数据量无关的,我们就称之为常数操作,反之和数量有关的,就不是一个常数操作
1.2 时间复杂度
- 时间复杂度为一个算法流程中,常数操作量的一个指标。常用O(读作big O )来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。
我们拿选择排序进行举例
- 选择排序的核心思想:
选择排序是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,继续放在起始位置知道未排序元素个数为0。
即:
1>首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
2>再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的起始位置。
3>重复第二步,直到所有元素均排序完毕。
问题来了,在以上过程中我们进行了多少的常数操作
假如有N个数,那么第一步操作,我们就要看N眼
也进行N次的比较,找到最小的数后我们我们把他交换到0位置上去,进行了一次交换
第二步操作,我们就要看N-1眼,也进行N-1次的比较,交换一次;
第三步操作,我们就要看N-2眼,也进行N-2次的比较,交换一次;
第四步…
所以推出:
看:N+N-1+N-2+N-3…等差数列
比:N+N-1+N-2+N-3…等差数列
交换:N次
所以:一共进行了aN²+bN+C常数操作
-
综上:时间复杂度就是在常数操作数量集的表达式中,B阶项项不要(bN+C),只要最高阶的项(aN²)而且忽略掉高阶系数所剩下的东西;所以我们说他是O(N²)的算法
-
在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为O(f(N)).
-
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数项时间”。
通常O(N)<O(N²),后者的时间复杂度大于前者,所以O(N)算法流程为优,即:谁小谁好,一样,比拼常数项操作
额外空间复杂度O(1)
- 当我们只需要额外的几个变量就可以实现相关的操作的时候额外空间复杂度就是O(1).
//选择排序
public static int[]selectionSort(int[] arr){
if (arr==null||arr.length <2) {
return arr;
}
for (int i = 0; i < arr.length-1; i++) {//i~N-1
int minIndex=i;
for (int j = i+1; j < arr.length; j++) {//i~N-1找出最小下标的值
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
}
swapselect(arr,i,minIndex);
}
return arr;
}
private static void swapselect(int[] arr, int i, int j) {
int t=arr[i];
arr[i]=arr[j];
arr[j]=t;
}
- 在上述代码中这里开辟出来了额外三个变量分别是I,j,minIndex
二、常见的排序算法
2.1冒泡排序
- 其思想是相邻的元素两两比较,较大的数下沉,较小的数冒起来,这样一趟比较下来,最大(小)值就会排列在一端。整个过程如同气泡冒起,因此被称作冒泡排序。
1>比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2>每趟从第一对相邻元素开始,对每一对相邻元素作同样的工作,直到最后一对。
3>针对所有的元素重复以上的步骤,除了已排序过的元素(每趟排序后的最后一个元素),直到没有任何一对数字需要比较.
代码实现
//冒泡排序
public static int[] bubbleSort(int[]arr){
if(arr==null||arr.length <2){
return arr;
}
for (int e = arr.length - 1; e > 0; e--) {
for (int i = 0; i < e; i++) {
if (arr[i] > arr[i+1]) {
swap(arr,i, i+1);
}
}
}
return arr;
}
//交换arr的i和j位置上的值
private static void swap(int[] arr, int i, int j) {
arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];
}
异或
上述swap方法是异或运算
- 异或运算,相同为0,不同为1
- 同或运算,相同为1,不同为0
异或可以记忆为无进位相加
1)0 ^N=N N ^N=0
2)异或运算满足交换律,结合律
a ^ b=b ^ a
3)同一批数去异或,谁先谁后结果都是一样的
上述三行代码跑完值是如何交换过来
假设:int a=甲,int b=已
a=a ^b; a=甲 ^已
b=a ^b; b=(甲 ^已) ^已 即b=甲
a=a ^b; a=(甲 ^已) ^甲 即a=已
能够使用上述运算有一个前提,交换的数据,必须是在内存中独立的俩块
异或练习
1)在一个数组int[]arr中,已知只有一种数出现了奇数次,剩下的所有数都出现了偶数次,怎么找到出现了奇数次的数 (o(N))
2)在一个数组int[]arr中,已知只有2种数出现了奇数次,剩下的所有数都出现了偶数次,怎么找到出现了奇数次的数(o(N))
解:
//1.
//int eor=0;
//eor^int[]arr;
//异或运算,同一批数去异或,谁先谁后结果都是一样的
//所以异或完之后的数就是奇数
public static void printOddTimesNum1(int[]arr){
int eor=0;
for (int cur : arr) {
eor^=cur;
}
System.out.println(eor);
}
//2.
//int eor=0;执行第一问的操作;结果肯定是俩个不同的奇数异或,这里我们就用a ^b来展示俩个不同的奇数异或;即eor=a ^b;且eor!=0,假设eor必有一个位置上是1
//int rigthtOne=eor&(~eor+1);提取出最右的1 ~取反
//int onlyOne=0; eor'=a或b
//eor^eor'=a或b
public static void printOddTimesNum2(int[] arr) {
int eor=0;
for (int cur : arr) {
eor^=cur;
}
int rightOne=eor&(~eor+1);//提取出最右的1 ~取反
int onlyOne=0;
for (int cur : arr) {
if ((cur &rightOne)==1) {//提取出数组中第八位上都是是1的数
onlyOne^=cur;//提取结束后,开始异或这时最后只会剩下一位第八位是1的数,那么这个数就是a或b;
}
}
System.out.println(onlyOne+" "+(eor^onlyOne));
}
2.2插入排序
- 插入排序也是一种常见的排序算法,插入排序的思想是:将初始数据分为有序部分和无序部分,每一步将一个无序部分的数据插入到前面已经排好序的有序部分中,直到插完所有元素为止。
插入排序的步骤如下:每次从无序部分中取出一个元素,与有序部分中的元素从后向前依次进行比较,并找到合适的位置,将该元素插到有序组当中。
如:打斗地主时,拿到的牌跟你手上的牌,从左到右或是从右到左,到适合的位置时就插入进去,然后再揭下长牌
- 插入排序,数据状况不同,时间复杂度是不同的
例:int []arr={7,6,5,4,3,2,1}; O(N ^²)
int []arr={1,2,3,4,5,6,7};O(N)
- 遇到算法复杂度时,永远按最差的复杂度进行计算,所以插入排序是O(N ^²);
//插入排序
public static int[]insertionSort(int[] arr){
if (arr==null||arr.length <2) {
return arr;
}
for (int i = 1; i < arr.length; i++) {//0~i上做到有序
for (int j = i-1; j >=0&&arr[j]>arr[j+1]; j--) {//j位置上比他下一位数小进行换位且不能越界
swapselect(arr,j,j+1);
}
}
return arr;
}
private static void swapselect(int[] arr, int i, int j) {
int t=arr[i];
arr[i]=arr[j];
arr[j]=t;
}
2.3二分查找
-
二分查找(Binary search)也称折半查找,是一种效率较高的查找方法。但是,二分查找要求线性表中的记录必须按关键码有序,并且必须采用顺序存储。
-
问题一:在一个有序数组中,找到某个数是否存在,最直接的方法就是遍历一遍,有就返回true,没有就返回false,这时的时间复杂度为O(N);而二分查找法每次折半进行查找的时间复杂度为O(log2N)简写为O(logN);
-
问题二:在一个有序数组中,找>=某个数最左侧的位置
-
问题三:在一个无序的数组中,相邻的数一定不相等,找到该数组中最小的数,时间复杂度能否好过O(N);
闭区间内仍存在最小值,如果一个数为0到N-1上,二分的点为M,如果M比M-1小还比M-2小,那么直接返回M,如果M位置比M-1大,则0–M的位置上一定存在最小值