数据结构与算法入门_第02期笔记

1. 学习回顾

  • 时间:2020年2月17日 ~ 2月23日
  • 本周学习时长:7 次,共 9 小时
  • 学习主要内容:

    • 完成《算法图解》后两章的学习:

      • KNN算法,简单说就是一种“近朱者赤,近墨者黑”的算法,如,Netflix的推荐系统,Google的OCR数字化,以及机器学习,都会用到KNN算法
      • 更多的算法及其应用场景,如,对比论文抄袭可以使用Simahash算法,面对海量数据但内存有限时刻可考虑HyperLogLog算法,想研究对称加密可以先从Diffie-Hellman开始,线性规划是一个非常酷的算法(所有的图算法都可以用它来实现)
    • 并开始《大话数据结构》的学习:

      • 数据结构基本概念的学习,逻辑结构是面向具体的问题的,有集合线性树形图形四种结构;而物理结构是面向计算机的,是逻辑结构在计算机上的存储,有顺序链式两种存储结构
      • 算法复杂度的由来,什么是算法复杂度,如何使用算法复杂度;由于算法复杂度是一个重要的概念,所以,下面会结合《极客时间--数据结构与算法之美》这门课程中的第3、4章内容来进行总结

2. 算法复杂度

2.1. 相关概念
  • 时间复杂度(渐进时间复杂度 asympotic time complexity)

    • 代码执行时间随数据规模增长的趋势变化
    • 常见时间复杂度有,常量阶 O(1),对数阶 O(logn),线性阶 O(n),线性代数阶 O(nlogn),平方阶 O(n^2)、立方阶 O(n^3)、...K次方阶 O(n^k),这些是多项式量级;指数阶 O(2^n),阶乘阶 O(n!),这些是非多项式量级
  • 空间复杂度(渐进空间复杂度 asympotic space complexity)

    • 算法的存储空间与数据规模之间的增长关系
    • 常见的空间复杂度有,常量阶 O(1),线性阶 O(n),平方阶 O(n^2)
  • 四个复杂度分析方面的知识点

    • 最好情况时间复杂度(best case time complexity)
    • 最坏情况时间复杂度(worst case time complexity)
    • 平均情况时间时间复杂度(average case time complexity)
    • 均摊时间复杂度(amortized time complexity),大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系
2.2. 实战练习
案例1: 在一个无序的数组(array)中,查找变量 x 出现的位置。如果没有找到,就返回 -1。
分析以下代码的 最好情况时间复杂度最坏情况时间复杂度平均情况时间复杂度期望时间复杂度
// n 是数组 array 的长度
int find(int[] array, int n, int x) {
  int i = 0;
  int pos = -1;
  for (; i < n; ++i) {
    if (array[i] == x) {
       pos = i;
       break;   // 如果去掉此行,算法复杂度会变化么?
    }
  }
  return pos;
}
  • 综合分析:查找结果会有n+1种情况,在数组的0 ~ n-1位置和不在数组中,因此:

    • 最好情况时间复杂度:总是在最理想的情况下array[0]位置找到,O(1)
    • 最坏情况时间复杂度:总是在最糟糕的情况下array[n-1]找到或找不到,O(n)
    • 平均情况时间复杂度:最好和最坏是极端,根据n+1种情况的平均
      (1+2+3+...+n+n)/(n+1)=n(n+3)/2(n+1),即O(n)
    • 期望时间复杂度:根据概率,假设找的到和找不到的概率都为1/2
      ((1+2+3+...n)/n)1/2 + n1/2=(3n+1)/4,即O(n)
案例2:往数组中插入数据,当数组满了之后,也就是代码中的 count == array.length 时,用 for 循环遍历数组求和,并清空数组,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入;但如果数组一开始就有空闲空间,则直接将数据插入数组。
分析以下代码的 平均时间复杂度均摊时间复杂度
// array 表示一个长度为 n 的数组
// 代码中的 array.length 就等于 n
int[] array = new int[n];
int count = 0;

void insert(int val) {
   if (count == array.length) {
      int sum = 0;
      for (int i = 0; i < array.length; ++i) {
         sum = sum + array[i];
      }
      array[0] = sum;
      count = 1;
   }

   array[count] = val;
   ++count;
}
  • 综合分析:插入是有规律的,连续n次直接插入,后跟一次循环计算、清空、插入操作,因此:

    • 平均时间复杂度:根据上面的规律
      (1+1+1+...+1)/(n+1) + n/(n+1)=2n/(n+1),即O(1)
    • 均摊时间复杂度:每n次的O(1)操作,都会跟着一次O(n)操作,所以把耗时最多的这次操作均摊下来,复杂度就是O(1)
案例3:往数组中添加一个元素,如果数组空间不够了,生成一个新的数组是原来数组的两倍,并将原数组的元素依次拷贝过去。
分析以下代码的 均摊时间复杂度
// 全局变量,大小为 10 的数组 array,长度 len,下标 i。
int array[] = new int[10]; 
int len = 10;
int i = 0;
 
// 往数组中添加一个元素
void add(int element) {
   if (i >= len) { // 数组空间不够了
     // 重新申请一个 2 倍大小的数组空间
     int new_array[] = new int[len*2];
     // 把原来 array 数组中的数据依次 copy 到 new_array
     for (int j = 0; j < len; ++j) {
       new_array[j] = array[j];
     }
     // new_array 复制给 array,array 现在大小就是 2 倍 len 了
     array = new_array;
     len = 2 * len;
   }
   // 将 element 放到下标为 i 的位置,下标 i 加一
   array[i] = element;
   ++i;
}
  • 综合分析:添加元素是有规律的,连续10次添加操作后,会跟随一次生成长度20的数组,并循环10次进行拷贝到新数组的操作,然后是连续20次,接着是40次,80次...。

    • 均摊时间复杂度:根据上面的分析,连续n次的O(1)操作,跟随一次O(n)操作,然后是连续2n次O(1)操作,跟随一次O(2n)操作,分摊下来就是O(1)。
2.3. 小结

时间复杂度分析时,需要根据代码的实际执行情况来进行,要考虑到最好、最坏的情况;熟练掌握分析方法,除了多练习外,还需要掌握一定的数学概率知识,如平均值、期望值等。

你可能感兴趣的:(数据结构与算法,学习笔记,算法复杂度)