左程云笔记

Posted by Haiming on October 14, 2023

左程云的算法通关课 - YouTube

003 二进制和位运算

1. 一个简便的得到负数二进制位的方式

对应正数减1再按位取反:

比如-1, 就是( 0001 - 1 ) 再按位取反,变成 1111

当然,知道一个负数本身,想知道其十进制对应的是谁,就要先按位取反,然后再+1 就好了

2. 二进制的空间范围

对于一个n位的二进制数而言,其表达空间为 2^n, 非负数(0+所有正数)和负数的个数是相同的。

那么最小的值,比如-8, 相反数是没法得到的,其相反数会和自己相同。

同样的,对于一个整数来说,比如Integer的最小值,其取绝对值也还是自己

得到一个数的相反数,等于这个数按位取反然后+1

下面两个式子等价:

int a = (-b);
int a = (~b) + 1

3. | 的穿透性

|| 只有一路是false,才会从左向右穿透,不然会直接从左往右返回第一个值

但是| 而言,如果是 a|b ,那么a和b都会进行验证之后再来判断

对于 & 而言是一样的,如果是 & ,那么会穿透左右两边都执行再判断,而 &&就不会,第一个是false那么就会短路

4. 为什么要这样设计二进制

原因在于对于负数,正数互相相加的时候,只要忽略溢出位,可以统一按照二进制的加法来做,这样就避免了在相加时候的条件转移(如果是正数+负数/正数+正数……)

那么也可以看出来,在计算的时候,是否溢出应该是程序员本身来保证,计算机只是按照二进制位运算的规律进行计算而已,不负责校验是否溢出

为什么要让加法运算这么快

计算机内部本身并没有真正用来做加减乘除的单元,只有用来做位运算的单元,加减乘除本质实现都是优化之后的加法。

5 对数器

对数器的基本原理:

对于一个算法题目而言,暴力解好想,但是最优解可能出错。

那么构造一个对数器,其输入的长度和数值范围都是我们随机生成的(为了暴力解不超时,可以先构造范围小一些的),然后多次比较二者的输出,如果相同,那么就是没问题。不同的话就打印出来不同的数据集进行debug。

前提条件:保证自己的暴力解正确.

关键:找到一个数据量很小的错误样本

6. 二分搜索

在有序数组上面进行搜索。

个人认为,二分算法要点以下:

  1. 注意边界值,mid = l + (r-l)/2, 所以可能和l或者r相同。这种情况如果有 l-1 或者 r+1 的值的判断(比如峰值),就要注意是否可能会溢出边界。

  2. 对于可能溢出边界的值,直接进行一开始的枚举,把可能溢出的数据进行排除之后再进行逐步二分

  3. 每次二分之后,得到的中间值一般都在下一次循环之中舍去,取其左边或者右边作为下一次的边界。原因是要么不符合,要么已经在答案之中记录了

6.1 在有序数组确定num是否存在

对数器思路:直接从左往右遍历,有就是有,没有就是没有。

6.2 寻找峰值问题

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {
    public int findPeakElement(int[] nums) {
        int ans = -1, len = nums.length;
        if(nums == null || len ==0){
            return ans;
        }
        if(len == 1 ){
            return 0;
        }
        if(nums[0]>nums[1]){
            return 0;
        }
        if(nums[len-2]<nums[len-1]){
            return len-1;
        }

        int l=1, r= len-2, mid;
        while(l<=r){
            mid = l+(r-l)/2;
            if(nums[mid]<nums[mid-1]){
                r= mid-1;
            }else if(nums[mid]<nums[mid+1]){
                l=mid+1;
            }else{
                ans = mid;
                break;
            }
        }
        return ans;
    }
}

6.3 山脉数组的峰顶索引

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int ans = 0;
        int l = 1, r = arr.length - 2, mid;
        if (arr[1] > arr[0] && arr[1] > arr[2]) {
            return 1;
        }
        if (arr[r - 1] > arr[r] && arr[r - 1] > arr[r - 2]) {
            return r - 1;
        }
        while (l <= r) {
            mid = l + (r - l) / 2;
            if (arr[mid] < arr[mid - 1]) {
                r = mid - 1;
            } else if (arr[mid] < arr[mid + 1]) {
                l = mid + 1;
            } else {
                ans = mid;
                break;
            }
        }
        return ans;
    }
}

7. 时间复杂度和空间复杂度

时间复杂度

时间复杂度是本质上面的运算速度,所以只看最高项的部分,其他在数据量趋于无穷时候的低阶项相比较之下都可以忽略

随机流程和固定流程算法的时间复杂度

  1. 严格固定流程的算法,一律用最差情况进行估计

  2. 随机流程的方法,用平均值作为估计

    举例:生成一个相邻值不等的随机数组,如果是最差情况,永远都是得到同样的值,那么就无法进行下去,复杂度是无穷大;应该按照期望来看,是O(N)

空间复杂度

强调额外空间.

均摊复杂度

动态数组的扩容,如果要放入N个数,其扩容次数小于 2N,均摊到每一个数字上面,其复杂度为 O(1)

调和级数

不要用代码结构来判断时间复杂度,比如:N/1 + N/2 + N/3 + … + N/N,这个流程的时间复杂度是O(N * logN),著名的调和级数

常见时间复杂度

O(1) O(logN) O(N) O(N*logN) O(N^2) … O(N^k) O(2^N) … O(k^N) … O(N!)