(一)认识时间复杂度
常数时间的操作
一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的一个指标。常用 O(读作 big O)来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。
在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为 f(N),那么时间复杂度为 O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是"常数项时间"。
常数项时间示例:
java
package class001;
public class time {
public static void process1(){
int N=1000;
int a=0;
for(int i=0;i<N;i++){
a=2+5;
a=4*7;
a=6*8;
}
}
public static void process2(){
int N=1000;
int a=0;
for(int i=0;i<N;i++){
a=3|6;
a=3&4;
a=4^785;
}
}
public static void main(String[] args){
}
}
(二)异或运算
1.异或运算的性质
- 0^N=N,N^N=0
- 交换律,结合律:a^b=b^a,(a^b)^c=a^(b^c)
- 同一串数字异或的结果是确定的,和它们的异或次序无关
2.异或运算的应用
(1)交换两个数的值,节约额外空间开销
java
int a=17;
int b=23;
a=a^b;//a=甲^23,b=乙
b=a^b;//a=甲^乙,b=甲^乙^乙=甲^0=甲
a=a^b;//a=甲^乙^甲=0^乙=乙,b=甲
(2)使用异或交换数组中的两个元素时,如果两个位置实际上指向同一个存储单元,例如 i == j,异或交换会把该元素变成 0。故谨慎使用。
示例:
java
package class001;
public class or {
public static void main(String[] args) {
int a=10;
int b=18067;
a=a^b;
b=a^b;
a=a^b;
System.out.println(a);
System.out.println(b);
System.out.println("--------------------------");
int[] arr = {10, 20};
int i = 0;
int j = 1;
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println("--------------------------");
int[] arr1 = {10, 20, 30};
int i1 = 1;
int j1 = 1;
arr1[i1] = arr1[i1] ^ arr1[j1];
arr1[j1] = arr1[i1] ^ arr1[j1];
arr1[i1] = arr1[i1] ^ arr1[j1];
System.out.println(arr1[1]);
}
}
输出:
java
18067
10
--------------------------
20
10
--------------------------
0
3.题目:
在一个整型数组中,(1)只有一种数字出现了奇数次,其它所有数出现了偶数次,怎么找到出现了奇数次的数?(2)有两种数出现了奇数次,其它所有数出现了偶数次,如何找到这两种数?要求:时间复杂度O(n),空间复杂度O(1)
分析:(1)由异或不变性可知,纯偶数项会相互抵消变成0,而奇数项(设总数为N)的N-1个偶数项异或后会变成0,0和这个最后的奇数项异或则是这个出现奇数次的数本身。
(2)设a和b是两个出现奇数次(m,n)的数,其它数都出现偶数次,其它数的偶数项在异或完后会抵消,而a和b的偶数项(m-1,n-1)则会相互抵消,最后只剩下eor=a^b这个结果;由于a!=b,则eor=a^b!=0,由二进制可知,eor(设二进制eor存在k+i位)必然有一位不等于0,设这一位为第k位,那么a和b的第k位必然不相等;再准备一个变量eor*,让它继续异或数组中的数,但我们只让eor*异或数组中第k位是1的那些数字,可以把数组分为"第k位是1""第k位是0"的两类,此时a和b自然被分再这两类中,最终得到的这个数字是a或b中的一个,不确定,但遍历数组可以得到它是eor*=a or b;另一个出现奇数次的数可以通过eor^eor*得出。另,对于"标志位k",我们可以取k是最低位即取最右侧的1,示例:eor=1010111100,~eor=0101000011,~eor+1=0101000100,k=eor &(~eor+1)=0000000100。
代码:
java
package class001;
public class Code_EvenTimesOddTimes {
public static void printOddTimesNum1(int[] arr){
int eor=0;
for(int cur: arr){
eor=eor^cur;
}
System.out.println("a="+eor);
}
public static void printOddTimesNum2(int[] arr){
int eor=0;
for (int i=0;i<arr.length;i++){
eor =eor^arr[i];
}
//eor=a^b
//eor!=0
//eor必然有一个位置上是1
int onlyOne=0;//eor*
int rightOne=eor &(~eor+1);//提出最右的1
for (int cur:arr){
if((cur & rightOne)==0){
onlyOne^=cur;
}
}
System.out.println("a="+onlyOne+" "+"b="+(eor^onlyOne));
}
public static void main(String[] args){
int []arr1={1,1,4,4,5,6,6};
int []arr2={1,4,4,5,5,6,6,7};
printOddTimesNum1(arr1);
printOddTimesNum2(arr2);
}
}
运行结果:
java
a=5
a=1 b=7
(三)插入排序细节的讲解和复杂度分析
时间复杂度O(N^2),额外空间复杂度O(1)
算法流程按照最差情况来估计时间复杂度
java
package class001;
import java.util.Arrays;
public class Code_InsertionSort {
public static void insertionSort(int[] arr){
if (arr==null || arr.length<2){
return;
}
//o~0有序
//0~i想有序
for (int i=1;i<arr.length;i++){//0~i做到有序
for (int j=i-1;j>=0 && arr[j]>arr[j+1];j--){
swap(arr,j,j+1);
}
}
}
//i和j是同一个位置的话,会出错
public 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];
}
//for test
public static void comparator(int[] arr){
Arrays.sort(arr);
}
public static void main(String[] args){
int []arr1={1,2,3,4};
int []arr2={11,43,32,12,24};
System.out.println("排序前:"+Arrays.toString(arr1));
insertionSort(arr1);
System.out.println("排序后:"+Arrays.toString(arr1));
System.out.println("----------------------------");
System.out.println("排序前:"+Arrays.toString(arr2));
insertionSort(arr2);
System.out.println("排序后:"+Arrays.toString(arr2));
}
}
运行结果:
java
排序前:[1, 2, 3, 4]
排序后:[1, 2, 3, 4]
----------------------------
排序前:[11, 43, 32, 12, 24]
排序后:[11, 12, 24, 32, 43]
(四)二分法的详解与拓展
(1)在一个有序数组中,找某个数是否存在
(2)在一个有序数组中,找>=某个数最左侧的位置
(3)局部最小值问题
(五)对数器的概念和使用
1,有一个你想要测的方法a
2,实现复杂度不好但是容易实现的方法b
3,实现一个随机样本产生器
4,把方法a和方法b跑相同的随机样本,看看得到的结果是否一样。
5,如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者方法b
6,当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
对数器示例:
java
package class001;
public class Validator {
// 为了验证
public static void main(String[] args) {
// 随机数组最大长度
int N = 200;
// 随机数组每个值,在1~V之间等概率随机
int V = 1000;
// testTimes : 测试次数
int testTimes = 50000;
System.out.println("测试开始");
for (int i = 0; i < testTimes; i++) {
// 随机得到一个长度,长度在[0~N-1]
int n = (int) (Math.random() * N);
// 得到随机数组
int[] arr = randomArray(n, V);
int[] arr1 = copyArray(arr);
int[] arr2 = copyArray(arr);
int[] arr3 = copyArray(arr);
selectionSort(arr1);
bubbleSort(arr2);
insertionSort(arr3);
if (!sameArray(arr1, arr2) || !sameArray(arr1, arr3)) {
System.out.println("出错了!");
// 当有错了
// 打印是什么例子,出错的
// 打印三个功能,各自排序成了什么样
// 可能要把例子带入,每个方法,去debug!
}
}
System.out.println("测试结束");
}
// 为了验证
// 得到一个随机数组,长度是n
// 数组中每个数,都在1~v之间,随机得到
public static int[] randomArray(int n, int v) {
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
// Math.random() -> double -> [0,1)范围山的一个小数,0.37463473126、0.001231231,等概率!
// Math.random() * v -> double -> [0,v)一个小数,依然等概率
// (int)(Math.random() * v) -> int -> 0 1 2 3 ... v-1,等概率的!
// (int) (Math.random() * v) + 1 -> int -> 1 2 3 .... v,等概率的!
arr[i] = (int) (Math.random() * v) + 1;
}
return arr;
}
// 为了验证
public static int[] copyArray(int[] arr) {
int n = arr.length;
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
ans[i] = arr[i];
}
return ans;
}
// 为了验证
public static boolean sameArray(int[] arr1, int[] arr2) {
int n = arr1.length;
for (int i = 0; i < n; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// 数组中交换i和j位置的数
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 选择排序
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int minIndex, i = 0; i < arr.length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
swap(arr, i, minIndex);
}
}
// 冒泡排序
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int end = arr.length - 1; end > 0; end--) {
for (int i = 0; i < end; i++) {
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);
}
}
}
}
// 插入排序
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1);
}
}
}
}