时间复杂度、空间复杂度、排序、异或运算

211 阅读7分钟

时间复杂度

  • 常数时间操作:
  1. 算数运算:+ - * /
  2. 位运算:>>(带符号右移动)、 >>>(不带符号右移动) 、 <<、 | 、& 、^

    ==带符号就是最高位补符号位,不带符号就是最高位补0==

  3. 赋值操作:比较,自增,自减操作
  4. 数组寻址等

执行时间固定的操作都是常数时间的操作。反之,都不是常数时间的操作

  • 通过基本动作的常数时间,推导时间复杂度

对于双层循环来说,n*(常数)+ (n-1)(常数)+ ... + 2(常数) + 1*(常数) => 推导出

y = an^2 + bn + c

忽略掉低阶项,忽略掉常数项,忽略掉高阶项的系数,得到时间复杂度为n^2

排序操作

1.选择排序

public static void selectionSort(int[] arr) {
  for (int i = 0; i < arr.length - 1; i++) {
    int minPos = i;
    for (int j = i + 1; j < arr.length; j++) {
      minPos = arr[j] < arr[minPos] ? j : minPos;
    }
    swap(arr, i, minPos);
  }
}

static void swap(int[] arr, int i, int j) {
  int temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

public static void main(String[] args) {
  int[] arr = {5, 3, 6, 8, 1, 7, 9, 4, 2};
  selectionSort(arr);
  for (int i = 0; i < arr.length; i++) {
    System.out.print(arr[i] + " ");
  }
}

时间复杂度(n-1)+ (n-2)+…+ 2 + 1 = n * (n-1)/2次

2.冒泡排序

static void bubbleSort(int[] a) {
  for (int i = a.length - 1; i > 0; i--)
    for (int j = 0; j < i; j++) {
      if (a[j] > a[j + 1]) {
        swap(a, j, j + 1);
      }
    }
}

static void swap(int[] a, int i, int j) {
  int temp = a[i];
  a[i] = a[j];
  a[j] = temp;
}

public static void main(String[] args) {
  int[] a = {9, 3, 1, 4, 6, 8, 7, 5, 2};
  bubbleSort(a);
  for (int i = 0; i < a.length; i++) {
    System.out.print(a[i] + " ");
  }
}

时间复杂度

3.插入排序

static void insertionSort(int[] a) {
  for (int i = 1; i < a.length; i++) {
    for (int j = i; j > 0 && a[j] < a[j - 1]; j--) {
      //if(a[j] < a[j-1]) {
      swap(a, j, j - 1);
      //}
    }
  }
}

static void swap(int[] a, int i, int j) {
  int temp = a[i];
  a[i] = a[j];
  a[j] = temp;
}
public static void main(String[] args) {
  int[] a = {9, 3, 1, 4, 6, 8, 7, 5, 2};
  insertionSort(a);
  for (int i = 0; i < a.length; i++) {
    System.out.print(a[i] + " ");
  }
}

插入排序和前面两种排序的不同是在于,插入排序跟数组初始顺序有关,在初始有序的情况下,有可能时间复杂度为O(N),有可能为O(N ^2),但是我们估计时间复杂度要按照最差的情况来估计,所以插入排序的时间复杂度仍然O(N ^2)

空间复杂度

申请有限几个变量,和样本量n没关系,就是空间复杂度O(1),如果要开辟一个空间数组和样本量n是一样大,用来支持我们的算法流程那么O(N)。反之用户就是要实现数组拷贝,我们开辟一个新的n大小数组用来支撑用户的需求,那么仍然是O(1)

常数项时间复杂度

如果两个相同时间复杂度的算法要比较性能,这个时候需要比较单个常数项时间,对能力要求较高,没有意义,不如样本量试验实际测试来比较

算法最优解

我们认为最优解的考虑顺序是,先满足时间复杂度指标,再去使用较少的空间。一般来说,算法题,ACM等不会卡常数项时间

常见时间复杂度

依次从好到坏 O(1) -> O(logN) -> O(N) -> O(N*logN) -> O(N^2) -> O(N^3) ... -> O(N!)

认识二分法

  1. 在一个有序数组中,找某个数是否存在

二分查找值,基于有序数组,算法复杂度为二分了多少次,O(log2N)可以写成O(logN) 123579

private static int binarySearch(int[] arr, int target){
  int left = 0, right = arr.length - 1, mid;
  while (left <= right) {
    mid = left + (right -left)/2;
    if(arr[mid] == target) {
      return mid;
    } else if(arr[mid] > target) {
      right = mid - 1;
    } else if(arr[mid] < target) {
      left = mid + 1;
    }
  }
  return -1;
}

public static void main(String[] args) {
  int a[] = { 0,1,2,3,4,5};
  System.out.println(binarySearch(a,4));//4
}
  1. 在一个有序数组中,找>=某个数最左侧的位置

122222333578888999999 找大于等于2最左侧的位置

package class01;

import java.util.Arrays;

public class Code05_BSNearLeft {

 // 在arr上,找满足>=value的最左位置
 public static int nearestIndex(int[] arr, int value) {
  int L = 0;
  int R = arr.length - 1;
  int index = -1; // 记录最左的对号
  while (L <= R) {
   int mid = L + ((R - L) >> 1);
   if (arr[mid] >= value) {
    index = mid;
    R = mid - 1;
   } else {
    L = mid + 1;
   }
  }
  return index;
 }

 // for test
 public static int test(int[] arr, int value) {
  for (int i = 0; i < arr.length; i++) {
   if (arr[i] >= value) {
    return i;
   }
  }
  return -1;
 }

 // for test
 public static int[] generateRandomArray(int maxSize, int maxValue) {
  int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
  for (int i = 0; i < arr.length; i++) {
   arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
  }
  return arr;
 }
 
 // for test
 public static void printArray(int[] arr) {
  if (arr == null) {
   return;
  }
  for (int i = 0; i < arr.length; i++) {
   System.out.print(arr[i] + " ");
  }
  System.out.println();
 }

 public static void main(String[] args) {
  int testTime = 500000;
  int maxSize = 10;
  int maxValue = 100;
  boolean succeed = true;
  for (int i = 0; i < testTime; i++) {
   int[] arr = generateRandomArray(maxSize, maxValue);
   Arrays.sort(arr);
   int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
   if (test(arr, value) != nearestIndex(arr, value)) {
    printArray(arr);
    System.out.println(value);
    System.out.println(test(arr, value));
    System.out.println(nearestIndex(arr, value));
    succeed = false;
    break;
   }
  }
  System.out.println(succeed ? "Nice!" : "Fucking fucked!");
 }

}
  1. 在一个有序数组中,找<=某个数最右侧的位置

package class01;

import java.util.Arrays;

public class Code05_BSNearRight {

 // 在arr上,找满足<=value的最右位置
 public static int nearestIndex(int[] arr, int value) {
  int L = 0;
  int R = arr.length - 1;
  int index = -1; // 记录最右的对号
  while (L <= R) {
   int mid = L + ((R - L) >> 1);
   if (arr[mid] <= value) {
    index = mid;
    L = mid + 1;
   } else {
    R = mid - 1;
   }
  }
  return index;
 }

 // for test
 public static int test(int[] arr, int value) {
  for (int i = arr.length - 1; i >= 0; i--) {
   if (arr[i] <= value) {
    return i;
   }
  }
  return -1;
 }

 // for test
 public static int[] generateRandomArray(int maxSize, int maxValue) {
  int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
  for (int i = 0; i < arr.length; i++) {
   arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
  }
  return arr;
 }

 // for test
 public static void printArray(int[] arr) {
  if (arr == null) {
   return;
  }
  for (int i = 0; i < arr.length; i++) {
   System.out.print(arr[i] + " ");
  }
  System.out.println();
 }

 public static void main(String[] args) {
  int testTime = 500000;
  int maxSize = 10;
  int maxValue = 100;
  boolean succeed = true;
  for (int i = 0; i < testTime; i++) {
   int[] arr = generateRandomArray(maxSize, maxValue);
   Arrays.sort(arr);
   int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
   if (test(arr, value) != nearestIndex(arr, value)) {
    printArray(arr);
    System.out.println(value);
    System.out.println(test(arr, value));
    System.out.println(nearestIndex(arr, value));
    succeed = false;
    break;
   }
  }
  System.out.println(succeed ? "Nice!" : "Fucking fucked!");
 }

}

  1. 局部最小值问题

无序数组,任意两个相邻的数不相等,返回一个局部最小值

package class01;

public class Code06_BSAwesome {

 public static int getLessIndex(int[] arr) {
  if (arr == null || arr.length == 0) {
   return -1; // no exist
  }
  if (arr.length == 1 || arr[0] < arr[1]) {
   return 0;
  }
  if (arr[arr.length - 1] < arr[arr.length - 2]) {
   return arr.length - 1;
  }
  int left = 1;
  int right = arr.length - 2;
  int mid = 0;
  while (left < right) {
   mid = (left + right) / 2;
   if (arr[mid] > arr[mid - 1]) {
    right = mid - 1;
   } else if (arr[mid] > arr[mid + 1]) {
    left = mid + 1;
   } else {
    return mid;
   }
  }
  return left;
 }

}

认识异或运算

异或运算:相同为0,不同为1 同或运算:相同为1, 不同为0,不掌握

上述特别不容易记住,异或运算就记成无进位相加:比如十进制6异或7,就理解为110和111按位不进位相加,得到001

  1. 所以 0^N = N , N^N = 0
  2. 异或运算满足交换律和结合律,所以A异或B异或C = A异或(B异或C) = (A异或C)异或B 题目一:如何不用额外变量就交换两个数
a = x b = y两个数交换位置

a = a ^ b # 第一步操作,此时 a = x^y , b=y
b = a ^ b # 第二步操作,此时 a = x^y , b = x^y^y => b = x^0 => b = x
a = a ^ b # 第三步操作,此时 a = x^y^x, b = x, a=> x^x^y => a=y

三步操作,实现交换ab的值

package class01;

public class Test {
 
 public static void main(String[] args) {
  int a = 6;
  int b = 6;
  
  a = a ^ b;
  b = a ^ b;
  a = a ^ b;
  
  System.out.println(a);
  System.out.println(b);
  
  int[] arr = {3,1,100};
  
  System.out.println(arr[0]);
  System.out.println(arr[2]);
  
  swap(arr, 0, 0);
  
  System.out.println(arr[0]);
  System.out.println(arr[2]);
 }
 
 public static void swap (int[] arr, int i, int j) {
  // arr[0] = arr[0] ^ arr[0];
  arr[i]  = arr[i] ^ arr[j];
  arr[j]  = arr[i] ^ arr[j];
  arr[i]  = arr[i] ^ arr[j];
 } 
}

注意,如果a和b指向同一块内存,改方法不可行

题目二:一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数

[2,2,1,3,2,3,2,1,1] 数组中存在四个2,两个3,三个1,定义一个常量等于0,分别对该数组中的数遍历一遍进行异或,最后,该变量等于多少,那么奇数的值就是多少。因为异或运算满足交换和结合律

题目三:怎么把一个int类型的数,提取出最右侧的1来

n与上(n取反加1)即可 => N & ( (~N)+1 )

题目四:一个数组中有两种不相等的数出现了奇数次,其他数出现了偶数次,怎么找到并打印这两种数

定义一个常量eor = 0,分别对该数组每个数异或,最终结果为a异或b,其中a和b就是这两个奇数,由于a!=b所以a异或b不等于0,即eor的值某一位上一定为1(有可能不止一个1随便选一个例如第八位),用该位做标记对原有数组的数进行分类,那么a和b由于第八位不相同一定被分开,再定义常量eor' = 0分别对第八位为0的数异或,那么得到的值,就是a和b其中一个,由于之前eor = a异或b,那么在用eor和eor'异或,就是另外一个值。一般来说,随便找一个1我们就找最右侧的那个1,如题目三

package class01;

public class Code07_EvenTimesOddTimes {

 // arr中,只有一种数,出现奇数次
 public static void printOddTimesNum1(int[] arr) {
  int eor = 0;
  for (int i = 0; i < arr.length; i++) {
   eor ^= arr[i];
  }
  System.out.println(eor);
 }

 // arr中,有两种数,出现奇数次
 public static void printOddTimesNum2(int[] arr) {
  int eor = 0;
  for (int i = 0; i < arr.length; i++) {
   eor ^= arr[i];
  }
  // eor = a ^ b
  // eor != 0
  // eor必然有一个位置上是1
  // 0110010000
  // 0000010000
  int rightOne = eor & (~eor + 1); // 提取出最右的1
  int onlyOne = 0; // eor'
  for (int i = 0 ; i < arr.length;i++) {
   //  arr[i] =  111100011110000
   // rightOne=  000000000010000
   if ((arr[i] & rightOne) != 0) {
    onlyOne ^= arr[i];
   }
  }
  System.out.println(onlyOne + " " + (eor ^ onlyOne));
 }

 public static int bit1counts(int N) {
  int count = 0;
  
  //   011011010000
  //   000000010000     1
  
  //   011011000000
  // 
  while(N != 0) {
   int rightOne = N & ((~N) + 1);
   count++;
   N ^= rightOne;
   // N -= rightOne
  }
  return count;
 }

 public static void main(String[] args) {
  int a = 5;
  int b = 7;

  a = a ^ b;
  b = a ^ b;
  a = a ^ b;

  System.out.println(a);
  System.out.println(b);

  int[] arr1 = { 3, 3, 2, 3, 1, 1, 1, 3, 1, 1, 1 };
  printOddTimesNum1(arr1);

  int[] arr2 = { 4, 3, 4, 2, 2, 2, 4, 1, 1, 1, 3, 3, 1, 1, 1, 4, 2, 2 };
  printOddTimesNum2(arr2);

 }

}