数据结构和算法

1 时间复杂度和空间复杂度

1.1 衡量算法优劣的标准

对于同一个问题,可以使用多种算法解决,如何衡量不同算法之间的优劣?主要从算法所占用的「时间」和「空间」两个维度去考量:

  • 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
  • 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述

1.2 时间复杂度

1.2.1 方式1:运行算法程序,统计所消耗的时间(不推荐)
  • 该种方法会受运行环境、数据规模等因素影响准确度,且十分不方便,因此不采用该方法来统计算法的时间复杂度
1.2.2 方式2:大O符号表示法(业界通用)
  • 大O符号表示法中,时间复杂度的公式为:

    T(n) = O( f(n) )
    # f(n) 表示每行代码执行次数之和
    # O 表示正比例关系,表示f(n)随n增长的一种趋势
    

    大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的

  • 计算原则:

    • 求出代码中每条语句执行的次数,算出每条语句执行次数之和
    • 进行化简:
      • 对于常量,时间复杂度为O(1),因为时间不随变量n的增长而增长
      • 对于高阶多项式,由于是分析n趋于无穷大时的趋势
        • 可以忽略低次项,只保留最高次项,如n²+2n+1,时间复杂度为O(n²)
        • 可以忽略最高次项的常量系数,如3n²,时间复杂度为O(n²)
  • 常见的时间复杂度量级有:

    常数阶O(1) < 对数阶O(logN) < 线性阶O(n) < 线性对数阶O(nlogN) < 平方阶O(n²) < 
    立方阶O(n³) < K次方阶O(n^k) < 指数阶(2^n)
    
  • 常数阶O(1)举例:

    int i = 1;
    int j = 2;
    ++i;
    j++;
    int m = i + j;
    // 代码消耗的时间并不随着某个变量的增长而增长,只要是没有循环等复杂结构,代码的时间复杂度就都是O(1)
    
  • 对数阶O(logN)

    int i = 1;
    while(i<n)
    {
        i = i * 2;
    }
    // 代码的执行次数为x,则2 的 x 次方等于 n,推出 x = log2^n,因此时间复杂度为O(logN)
    
  • 线性对数阶O(nlogN)

    for(m=1; m<n; m++)
    {
        i = 1;
        while(i<n)
        {
            i = i * 2;
        }
    }
    // 外层循环执行次数为n,内层循环执行次数为logN,因此时间复杂度为O(nlogN)
    
  • 线性阶O(n)

    for(i=1; i<=n; ++i)
    {
       j = i;
       j++;
    }
    // for循环里面的代码会执行n遍,消耗的时间是随着n的变化而变化的,这类代码都可以用O(n)来表示它的时间复杂度
    
  • 平方阶O(n²)

    for(x=1; i<=n; x++)
    {
       for(i=1; i<=n; i++)
        {
           j = i;
           j++;
        }
    }
    // 外层循环次数为n,内层循环次数为n,因此时间复杂度为O(n²)
    

1.3 空间复杂度

  • 空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势

  • 空间复杂度比较常用的有:O(1)、O(n)、O(n²)

  • 空间复杂度 O(1)

    int i = 1;
    int j = 2;
    ++i;
    j++;
    int m = i + j;
    // 代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
    
  • 空间复杂度 O(n)

    int[] m = new int[n]
    // new了一个数组出来,这个数据占用的大小为n,且占用大小随着n的增加而增加,因此空间复杂度为O(n)
    
  • 空间复杂度O(n²)

    int[][] m = new int[n][n]
    // new了一个数组出来,这个数据占用的大小为n²,且占用大小随着n的增加而增加,因此空间复杂度为O(n²)
    

2 排序算法

2.1 JavaScript排序方法

2.2 时间复杂度为O(n²)的排序算法

  • 排序算法是否稳定:相同元素的顺序在排序后不会发生改变的算法成为稳定的算法

  • 参考链接:https://zhuanlan.zhihu.com/p/42586566

  • 选择排序

    算法描述:

    1. 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
    2. 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
    3. 重复第二步,直到所有元素均排序完毕。

    排序是不稳定的

    public static int[] selectSort(int[] a) {
        int n = a.length;
        for (int i = 0; i < n - 1; i++) {
            int min = i;
            for (int j = i + 1; j < n; j++) {
                if(a[min] > a[j]) min = j;
            }
            //交换
            int temp = a[i];
            a[i] = a[min];
            a[min] = temp;
        }
        return a;
    }
    
  • 冒泡排序

    算法描述:

    1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
    2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
    3. 针对所有的元素重复以上的步骤,除了最后一个;
    4. 重复步骤1~3,直到排序完成。

    排序是稳定的

    public static void bubbleSort(int[] arr) {
        for (int i = arr.length - 1; i > 0; i--) { // 每次需要排序的长度
            for (int j = 0; j < i; j++) { // 从第一个元素到第i个元素
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    
  • 插入排序

    算法描述

    1. 把待排序的数组分成已排序和未排序两部分,初始的时候把第一个元素认为是已排好序的。
    2. 从第二个元素开始,在已排好序的子数组中寻找到该元素合适的位置并插入该位置。
    3. 重复上述过程直到最后一个元素被插入有序子数组中。

    排序是稳定的

    public static void insertionSort(int[] arr){
        for (int i=1; i<arr.length; ++i){
            int value = arr[i];
            int position=i;
            while (position>0 && arr[position-1]>value){
                arr[position] = arr[position-1];
                position--;
            }
            arr[position] = value;
        }
    }
    

3 字符串常用操作

4 二分查找

4.1 寻找一个数

  • 框架:

    public int binarySearch(int[] array, int target) {
        int left = 0;
        int right = array.length - 1;
        while (left <= right) { // 退出条件为 left = right + 1
            int mid = left + (right - left)/2;
            if (array[mid] < target) {
                left = mid + 1;
            } else if (array[mid] > target) {
                right = mid - 1;
            } else if (array[mid] == target) {
                return mid;
            }
        }
        return -1;
    }
    
    @Test
    public void binarySearchTest() {
        int[] array = {1,2,3,4,5,6};
        int target = 5;
        int index = binarySearch(array, target);
        System.out.println("index = " + index + ", value = " + array[index]);
    }
    

4.2 寻找左侧边界值

  • 框架

    public int binarySearchLeftBound(int[] array, int target) {
        int left = 0;
        int right = array.length - 1;
        while (left <= right) { // 退出条件为 left = right + 1
            int mid = left + (right - left)/2;
            if (array[mid] < target) {
                left = mid + 1;
            } else if (array[mid] > target) {
                right = mid - 1;
            } else if (array[mid] == target) {
                right = mid - 1; // 缩小右边界,以找到左边界
            }
        }
        if (left >= array.length || array[left] != target) {
            return -1;
        }
        return left;
    }
    
    @Test
    public void binarySearchLeftBoundTest() {
        int[] array = {1,2,3,3,3,3,3,3,3,4,5,6};
        int target = 3;
        int index = binarySearchLeftBound(array, target);
        System.out.println("index = " + index + ", value = " + array[index]);
    }
    

4.3 寻找右侧边界值

  • 框架

    public int binarySearchRightBound(int[] array, int target) {
        int left = 0;
        int right = array.length - 1;
        while (left <= right) { // 退出条件为 left = right + 1
            int mid = left + (right - left)/2;
            if (array[mid] < target) {
                left = mid + 1;
            } else if (array[mid] > target) {
                right = mid - 1;
            } else if (array[mid] == target) {
                left = mid + 1; // 扩大左边界,以找到右边界
            }
        }
        if (right < 0 || array[right] != target) {
            return -1;
        }
        return right;
    }
    
    @Test
    public void binarySearchRightBoundTest() {
        int[] array = {1,2,3,3,3,3,3,3,3,4,5,6};
        int target = 3;
        int index = binarySearchRightBound(array, target);
        System.out.println("index = " + index + ", value = " + array[index]);
    }
    

4.4 例题

5 DFS 深度优先搜索(先做例题,看能不能做出来)

5.1 框架

  • 思想:判断结束条件,for循环里递归,递归前做出选择,递归后撤销选择
  • 三要素:路径、选择列表(剪枝)、结束条件
  • 每个节点的属性:路径和选择列表
  • 注意事项:1. 数组要拷贝,要不然是同一个引用 2. 处理非重复元素可以不用visited 3. visited可以灵活选取,可以定义bool数组或者直接再给定的数组上操作(剪枝操作is_valid)

5.2 例题

6 BFS 广度优先搜索(先做例题,看能不能做出来)

6.1 框架

  • 问题本质:在一副图中,从起点到终点的最近距离
  • 思想:学会用层级的思想考虑问题
  • 核心数据结构:队列
  • 原理:单向扩散,从起点开始扩散,遍历当前层的时候将下一层入队
  • visited:起点加入,在当前节点的下一层节点入队时加入
  • 退出条件:当前值等于target

6.2 例题

posted @ 2022-01-15 05:44  青铜级程序员  阅读(28)  评论(0)    收藏  举报