时间和空间复杂度,简单排序,二分,对数器

138 阅读2分钟

时间和空间复杂度

  1. 什么是时间复杂度?时间复杂度怎么估算?

    • 确定算法流程的总操作数量与样本数量之间的表达式关系,只看表达式最高阶项的部分
  2. 何为常数时间的操作?

    • 如果一个操作的执行时间不以具体样本量为转移,每次执行时间都是固定时间。称这样的操作为常数时间的操作。
    • 常见的算术运算(+、-、*、/、% 等)
    • 常见的位运算(>>、>>>、<<、|、&、^等)
    • 赋值、比较、自增、自减操作等
    • 数组寻址操作
  3. 如何确定算法流程的总操作数量与样本数量之间的表达式关系?

    • 想象该算法流程所处理的数据状况,要按照最差情况来。
    • 把整个流程彻底拆分为一个个基本动作,保证每个动作都是常数时间的操作。 如果数据量为N,看看基本动作的数量和N是什么关系。
  4. 如何确定算法流程的时间复杂度?

    • 当完成了表达式的建立,只要把最高阶项留下即可。低阶项都去掉,高阶项的系数也去掉。记为:O(忽略掉系数的高阶项)
  5. 时间复杂度的意义

    • 当我们要处理的样本量很大很大时,我们会发现低阶项是什么不是最重要的;每一项的系数是什么,不是最重要的。真正重要的就是最高阶项是什么。
    • 这就是时间复杂度的意义,它是衡量算法流程的复杂程度的一种指标,该指标只与数据量有关,与过程之外的优化无关。
  6. 额外空间复杂度

    • 你要实现一个算法流程,在实现算法流程的过程中,你需要开辟一些空间来支持你的算法流程。
    • 作为输入参数的空间,不算额外空间。作为输出结果的空间,也不算额外空间。因为这些都是必要的、和现实目标有关的。所以都不算。
    • 除此之外,你的流程如果还需要开辟空间才能让你的流程继续下去。这部分空间就是额外空间。如果你的流程只需要开辟有限几个变量,额外空间复杂度就是O(1)。
  7. 一个问题的最优解是什么意思?

    • 一般情况下,认为解决一个问题的算法流程,在时间复杂度的指标上,一定要尽可能的低,先满足了时间复杂度最低这个指标之后,使用最少的空间的算法流程,叫这个问题的最优解。
    • 一般说起最优解都是忽略掉常数项这个因素的,因为这个因素只决定了实现层次的优化和考虑,而和怎么解决整个问题的思想无关。
  8. 复杂度排名从好到差:

    • O(1)
    • O(logN)
    • O(N)
    • O(N*logN)
    • O(N^2) O(N^3) … O(N^K)
    • O(2^N) O(3^N) … O(K^N)
    • O(N!)

选择排序和对数器

  1. 对数器的使用方法:对于一个问题,如果想出AB两种解法,就用随机大样本测试两种解法得到的结果是否相同。如果相同,则说明大概率两种方法都是对的。如果不同就缩小样本量,打印结果找bug,直到完全相同为止。
  2. 时间复杂度估算:很明显,如果arr长度为N,每一步常数操作的数量,如等差数列一般。所以,总的常数操作数量 = a*(N^2) + b*N + c (a、b、c都是常数)。所以选择排序的时间复杂度为O(N^2)。
public static void selectionSort(int[] arr) {
  if (arr == null || arr.length < 2) {
     return;
  }
  // 0 ~ N-1  找到最小值,在哪,放到0位置上
  // 1 ~ n-1  找到最小值,在哪,放到1 位置上
  // 2 ~ n-1  找到最小值,在哪,放到2 位置上
  for (int i = 0; i < arr.length - 1; i++) {
     int minIndex = i;
     for (int j = i + 1; j < arr.length; j++) { // i ~ N-1 上找最小值的下标 
        minIndex = arr[j] < arr[minIndex] ? j : minIndex;
     }
     swap(arr, i, minIndex);
  }
}

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

// for test
public static void comparator(int[] arr) {
  Arrays.sort(arr);
}

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

// for test
public static int[] copyArray(int[] arr) {

//    if (arr == null) {
//       return null;
//    }
  int[] res = new int[arr.length];
  for (int i = 0; i < arr.length; i++) {
     res[i] = arr[i];
  }
  return res;
}

// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
//    if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
//       return false;
//    }
//    if (arr1 == null && arr2 == null) {
//       return true;
//    }
  if (arr1.length != arr2.length) {
     return false;
  }
  for (int i = 0; i < arr1.length; i++) {
     if (arr1[i] != arr2[i]) {
        return false;
     }
  }
  return true;
}

// 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();
}

// for test
public static void main(String[] args) {
  int testTime = 500000;
  int maxSize = 100;
  int maxValue = 100;
  boolean succeed = true;
  for (int i = 0; i < testTime; i++) {
//       generateRandomArray不会产生空数组,顶多[],所以下面的测试方法都可以不判断是否为空
     int[] arr1 = generateRandomArray(maxSize, maxValue);
     int[] arr2 = copyArray(arr1);
     selectionSort(arr1);
     comparator(arr2);
     if (!isEqual(arr1, arr2)) {
        succeed = false;
        printArray(arr1);
        printArray(arr2);
        break;
     }
  }
  System.out.println(succeed ? "ys" : "no");

  int[] arr = generateRandomArray(maxSize, maxValue);
  printArray(arr);
  selectionSort(arr);
  printArray(arr);
}

冒泡排序

public static void bubbleSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   for (int e = arr.length - 1; e > 0; e--) { // 0 ~ e
      for (int i = 0; i < e; i++) {
         if (arr[i] > arr[i + 1]) {
            swap(arr, i, i + 1);
         }
      }
   }
}

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];
}

插入排序

如果某个算法流程的复杂程度会根据数据状况的不同而不同,那么必须要按照最差情况来估计。很明显,在最差情况下(比如:[4,3,2,1,0]),如果arr长度为N,插入排序的每一步常数操作的数量,还是如等差数列一般。所以,总的常数操作数量 = a*(N^2) + b*N + c (a、b、c都是常数)。所以插入排序排序的时间复杂度为O(N^2)。

public static void insertionSort(int[] arr) {
   if (arr == null || arr.length < 2) {
      return;
   }
   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];
}

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

经常见到的类型是在一个有序数组上,开展二分搜索。但有序真的是所有问题求解时使用二分的必要条件吗?不是。只要能正确构建左右两侧的淘汰逻辑,就可以二分。比如局部最小值问题。

public static boolean exist(int[] sortedArr, int num) {
   if (sortedArr == null || sortedArr.length == 0) {
      return false;
   }
   int L = 0;
   int R = sortedArr.length - 1;
   int mid = 0;
   // L..R
   while (L <= R) { // L..R 至少两个数的时候
      mid = L + ((R - L) >> 1);
      if (sortedArr[mid] == num) {
         return true;
      } else if (sortedArr[mid] > num) {
         R = mid - 1;
      } else {
         L = mid + 1;
      }
   }
   return false;
}

// for test
public static boolean test(int[] sortedArr, int num) {
   for(int cur : sortedArr) {
      if(cur == num) {
         return true;
      }
   }
   return false;
}


// 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;
}

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) != exist(arr, value)) {
         succeed = false;
         break;
      }
   }
   System.out.println(succeed ? "Nice!" : "Fucking fucked!");
}

在一个有序数组中,找>=某个数最左侧的位置

// 在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;
}

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

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;
}

局部最小值问题

  • arr长度为1时, arr[0]是局部最小
  • arr长度为N(N > 1)时, 如果arr[0] < arr[1], 那么arr[0]是局部最小
  • 如果arr[N-1] < arr[N-2]时, 那么arr[N-1]是局部最小
  • 如果0 < i< N-1, 既有 arr[i] < arr[i - 1], 又有arr[i] < arr[i + 1], 那么arr[i]是局部最小
  • 给定一个无序数组arr, 已知arr中任意两个相邻的数都不相等. 写一个函数, 只需返回arr中任意一个局部最小出现的位置即可
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;
}