每天一个算法(简单)


文章目录

前言

此篇文章的目的是在于建立一个可以协同配合的工作流,我希望对于一个算法的理解包含以下内容:详尽的分析,程序的结构,代码。文章中包含前两个部分,然后在本地完成代码。编写代码是否需要再附上结构,很难说,我现在想的是只加上主干,再附上一个链接即可

一.排序算法

二.查找算法

2.1 哈希查找

2.1.1 两数之和

题目:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

暴力解法:两层循环遍历所有的下标组合

优化:对于时空复杂度分别为n方和1的问题,优先考虑空间换时间
-在遍历的同时,记录一些信息,以省去一层循环,这是“以空间换时间"的想法
-需要记录已经遍历过的数值和它所对应的下标,可以借助查找表实现
-查找表有两个常用的实现:
·哈希表
·平衡二叉搜索树

因为不需要维护结果的顺序,因此使用哈希表

本质上,此问题是寻找target - x是否存在于数组中,暴力解法的问题就在于寻找target-x的时间复杂度太高,刚好哈希表查找的复杂度是o(1)

我犯了一个错误,虽然是找target-x, 但不是将target-x存到哈希表中,而是将数组中的元素逐个存到哈希表中。极端一点,我可以先将数组的元素使用哈希表存储,再遍历数组寻找target-x

程序上需要注意的是需要使用高级程序语言提供的哈希表,不需要自己构建哈希表
哈希表的使用
简单介绍哈希表:
哈希表是基于键、值对存储的数据结构,底层一般采用的是列表(数组)
使用除留余数法确定数据在哈希表中的位置,需要选择一个p,对P取余。P还决定了哈希表的长度,P要尽可能的原理2的幂
使用线性探测法处理冲突,比如两个数的哈希值相等,a占据位置之后,b以1为单位向后顺延

对于python,字典就是哈希表
enumerate()返回的是一个引用类型的变量,打印出来是这个变量的地址,属于enumerate类,需要类型转换为list.每个元素的顺序是,(下标,元素)

2.1.2 两个数组的交集

分析:很容易得出,目标是求出同时在两个数组的元素,且这个元素唯一

我最开始认为只需要将一个数组哈希化,但这样处理重复元素就很麻烦了。除非把ans数组也给哈希化,这样在推入一个新的交集元素之前才能以O(1)的性能判断该交集元素是否已经存在于ans. 哈希化的过程除了统计元素之外,也同时可以去除重复元素

2.1.3 日志速率限制器

又重新想了一下什么叫每x秒一次,从数值上来说,如果t发生了,那么下一次发生在t+x,这就是每x秒一次

2.2 最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

法一:横向扫描
依次遍历字符串数组中的每个字符串,对于每个遍历到的字符串,更新最长公共前缀,当遍历完所有的字符串以后,即可得到字符串数组中的最长公共前缀。
在这里插入图片描述
第一个字符串就作为最长公共前缀,再去遍历下一个字符串,更新最长公共前缀。一直迭代下去
时间:0(mn)

法二:纵向扫描
同上
这种方法最符合我的思考习惯

法三:分治法
法四:二分查找

2.3 KMP

2.3.1 实现 strStr()

题目:实现 strStr() 函数。

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

法一:暴力解法
思路很明确,双层循环。遍历主串的每一位,每访问到某一位时,遍历副串的每一位。只要有一个不对,退出内层循环

法二:KMP
以后再说

2.4 二分查找

前言

终止条件while l<=r, 为什么是小于等于???
A:
(1)对于偶数长度序列,在极端情况下,比如target在数组最后一个,会出现l=r的情况,此时l或者r指向的元素就是target
(2)对于奇数长度序列,在极端情况下,比如target在数组最后一个,同上
由此需要将中止条件设为while l<=r

2.4.1 搜索插入位置

题目:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

在一个有序数组中进行按值查找
在这里插入图片描述
这二分查找做下来,感觉处理边界不是很清晰

在这里插入图片描述
比如说退出循环的条件是left <= right,为什么后面返回left的值就可以了,left的值表示的是索引,不是数组下标

在不同情况下,究竟是在target<nums[mid]返回left,还是target>nums[mid]返回left
这些问题都需要解决

2.4.2 Sqrt(x)

题目:给你一个非负整数 x ,计算并返回 x 的 算术平方根 。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。

由于 xx 平方根的整数部分 \textit{ans}ans 是满足 k^2 \leq xk
2
≤x 的最大 kk 值,因此我们可以对 kk 进行二分查找,从而得到答案。

二分查找的下界为 0,上界可以粗略地设定为 xx。在二分查找的每一步中,我们只需要比较中间元素 mid 的平方与 xx 的大小关系,并通过比较的结果调整上下界的范围。由于我们所有的运算都是整数运算,不会存在误差,因此在得到最终的答案 ans 后,也就不需要再去尝试ans + 1

一般来说,二分查找的结果由mid来表示,之前找插入位置那个问题返回left是因为插入的位置不是mid的位置,而是left的位置

2.4.2 有效的完全平方数

一开始还没有意识到这是个查找问题,只是想到将原来的乘法问题转换成乘法问题,但本质上还是要找出一个符合条件的结果

一直以来对于二分查找不是特别熟悉

left <= right???
A:这是因为序列长度可能是奇数或者偶数,当left > right,对上述两种情况,循环都应该结束

mid的更新方式???
A:有两种。一般使用的是left + (right - low) / 2, 但实际上稍微合并一下就是第二种形式,(left + right) / 2

结果在哪里????
A:一般情况下,left指针表示最终的结果

2.4.3 第一个错误的版本

题目:你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

分析:
第一时间想到了二分查找,同时更新指针的依据要调整。就两句话:

  1. 正确版本之前的版本全部正确
  2. 错误版本之后的版本全部错误

不过,mid的值的计算我不太熟悉,又搞错了,以low指针的值作为base
mid = (high - low) // 2 + low

2.5 存在重复元素2

题目:给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。

分析:
下标之差在K以内的重复元素。等价于在长度为K+1的数组里存在重复元素。但是是否需要

原理很简单,但写成程序却很巧妙不容易想到
在这里插入图片描述
我之前的想法是每次向字典加入长度为K+1的元素,但是这里采用了一种类似于滚动的效果,先逐步加入元素,每次加入都要进行一次比较
当i <= k时,字典的长度从0 增长到k+1,每次增加元素都要判断是否存在重复

当i = k+1时,字典长度变为 k + 2, 为了维持窗口的长度,移除一个元素,移除元素的key对应于原数组的小标 i-k-1,然后比较此时的i是否存在重复。除i以外的元素前面已经比较过了,一定不存在重复。

三.数论相关

前言

对于一个大数,直接处理往往是很复杂的。通常,使用因式分解对大数进行拆分,对每一个质因数进行处理

自然数通常可以写成质因数相乘的形式

3.1 快速幂

https://blog.csdn.net/qq_19782019/article/details/85621386

这篇文章有非常详细地推导过程,最终给出了一个非常简单的结论:

最后求出的幂结果实际上就是在变化过程中所有当指数为奇数时底数的乘积

PS:需要注意的一点是,为了能够统一算法,也就是核心思想就是每一步都把指数分成两半,而相应的底数做平方运算,分两种情况处理:

  1. 当指数为偶数时,指数减半,底数平方

  2. 当指数为奇数时,抽出一个底数的一次方,另外一部分按照情况1处理

3.3 反转

3.3.1 整数反转(非常重要,涉及溢出)

题目:给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。如果反转后整数超过 32 位的有符号整数的范围 [− 2 31 2^{31} 231, 2 31 2^{31} 231 − 1] ,就返回 0。假设环境不允许存储 64 位整数(有符号或无符号)

像反转类的问题,可以马上想到使用栈来存储整数的每一位。当然也可以不增加栈这一额外的空间,无论怎样,反转问题都涉及到两个专业名词,【弹出】, 【推入】

即将整数当前的最后一位弹出整数,再推入反转数
其表达式也非常熟悉了:

取模的时候涉及到另外一个问题,负数的取模和取余
我熟悉的是两个整数,取模和取余的结果是一样的,余数是在除数范围内,比较符合直觉。两个负数,符号约掉了

当符号不一致的时候,高级程序语言就有一些设定,求模运算c = -2(向负无穷方向舍入),求余运算则c = -1(向0方向舍入)a = -7;b = 4, 求模时r = 1,求余时r = -3
当符号不一致时,结果不一样。求模运算结果的符号和b(除数)一致,求余运算结果的符号和a(被除数)一致
经过测试,在C/C++, C#, JAVA, PHP这几门主流语言中,%运算符都是做取余运算,而在python中的%是做取模运算

取模:商向负无穷方向,余数符号和除数一致
取余:商向0方向,余数符号和被除数一致

python确实特别,尤其是在本题的场景下,如果x<0,那么余数为正,显然是无法反映x的最后一位数字,需要利用算式新余数 = 余数 - 除数 来计算新的余数。负数取余真的反直觉
python负数的取模和整除都需要特别注意,取模不像正数可以直接得到最后一位,整除会向下取整

还有一点就是负数的反转,每次取最后一位都是取的负数,把推入的公式走一遍就清楚了

题目限制了反转数字的上下限,推入操作可能会造成溢出问题。一方面是x10造成的溢出,一方面是加上弹出的那一个数字造成的溢出
因此,在进行推入之前,需要进行两方面的判断,只要有一个部分造成溢出,那么就return
通过推导得到判断是否溢出的条件:
在这里插入图片描述

3.3.2 回文数

题目:给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。

解法一:将整数转为字符串
只需要比较字符串的前半段和后半段是否一致即可,但是需要额外的存储空间

解法二:将数字本身反转
和之前反转数字一样,需要考虑溢出的问题
在这里插入图片描述
如果直接进行反转,很明显会遇到溢出
于是可以通过只反转一半的数字来规避这个问题,反转问题是有序的,存在弹出和推入两个操作,从数字的末端开始操作

迭代终止条件的选择:
对于回文数而言,迭代过程中,右边一半是一定小于左边的,当中间数字被推入右边,那么右边就会大于等于左边,此时就可以终止
回文数一定是要等到处理中间位置的数字时才可以进行判断

相较于整数反转,不需要处理负数取模确实简单了不少

3.3.3 回文排列

题目:给定一个字符串,判断该字符串中是否可以通过重新排列组合,形成一个回文字符串。

分析:需要分析回文序列的特点,在本问题上也就是单词频率
奇数序列:频率中只有一个是奇数频率
偶数序列:频率中全部是偶数频率

3.4 映射规则

3.4.1 罗马字符转数字

在这里插入图片描述
需要建立一张转换表
其次,因为存在4,9等特殊情况,从左到右扫描字符串,判断当前字符和下一位字符代表数字的大小关系
其他情况,从左到右相加即可

3.4.2 同构字符串

题目:给定两个字符串 s 和 t,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

解法:使用两个哈希表维护两个字符串的双射关系
在这里插入图片描述

3.4.3 单词规律

题目:给定一种规律 pattern 和一个字符串 str ,判断 str 是否遵循相同的规律。

这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 str 中的每个非空单词之间存在着双向连接的对应规律。

分析:
这题干真抽象。不给示例根本看不懂。其实就是判断字符与字符串之间是否恰好对应
简单说就是在两个哈希表中需要形成这样的关系,a:b, b:a, 必须同时满足

3.5 大数运算

3.5.1 加1

题目:给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

需要注意的是,此问题只进行加1操作,因此发生进位的情况很有限
1.全为9,那么最前面进一位,后面全改0
2.找到9之前第一个非9元素,进一位,非9元素后面全改0。此时,非9元素后面一定是全为9的

情况2涵盖了9零散或者连续分布的情况

3.5.2 二进制求和

题目:给你两个二进制字符串,返回它们的和(用二进制表示)。

输入为 非空 字符串且只包含数字 1 和 0。

主要是实现加法的机制:
carry表示进位的值,比如说十进制里边,9+1进位为1,那么carry = 1.
对齐末位,每一位的计算结果为carry + a[i] + b[i] % 2,这个2表示进行的2进制运算。每一位计算的进位为carry + a[i] + b[i] // 2.这就是加法运算的核心

另外有两点,对齐末位,在程序中通过反转实现
两个加数不一定同样长,采取高位补0

3.5.3 阶乘后的零

题目:给定一个整数 n ,返回 n! 结果中尾随零的数量。

提示 n! = n * (n - 1) * (n - 2) * … * 3 * 2 * 1

解法:
对于任意一个 n! 而言,其尾随零的个数取决于展开式中 1010 的个数,而 10可由质因数 2 * 5 而来,因此 n!的尾随零个数为展开式中各项分解质因数后「2的数量」和「5的数量」中的较小值。
在这里插入图片描述
在这里插入图片描述

3.6 快乐数

题目:编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:

对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 true ;不是,则返回 false 。

解法:这个题很有意思
据分析,有三种可能的情况:
在这里插入图片描述在这里插入图片描述
通过上面的表格可以看出,4位及以上的数字通过操作会变回三位数,再操作一次就落到243以内,也就是说最坏的情况下,算法可能会在 243 以下的所有数字上循环,然后回到它已经到过的一个循环或者回到 1,不会无止境的变大

也就是说序列不是成环就是变成1的循环

之前我们做过循环链表的判定,可以使用双指针法(快慢)来判断是否成环。区别在于,循环链表在判断之前已经存在,本题需要在生成序列的过程中判断是否成环
或者说本题不需要生成序列,因为后面的节点由前面的节点生成,只需要更新双指针的值即可。

3.7 素数

3.7.1 计数素数

经典的素数筛问题哈

首先是素数的定义,在正整数范围内,大于1并且只能被1和自身整除的数

法一:遍历 2-n-1的所有数,看n是否能被整除
法二:对法一的范围进行优化,优化到 2-sqrt(n)的范围

PS:从法三开始,需要准备一个数组,保存i是否为素数的结果,默认全为素数。从法三开始,将法一法二的除法问题,变成了从小至大的乘法问题

官方解释
法三:埃氏筛
前两种方法求解每个数是否是质数的操作都是独立的,存在大量重复计算。该问题的基本开销在于判断一个数是否是质数,时间复杂度基于总共需要做多少次这样的操作
埃氏筛基于这样一个事实,如果 x是质数,那么大于 x的 x的倍数 2x,3x, 一定不是质数。
介绍什么是合数:
在这里插入图片描述
因此,我们设isPrime[i]表示数 i 是不是质数,如果是质数则为1,否则为0。从小到大遍历每个数,如果这个数为质数,则将其所有的倍数都标记为合数(除了该质数本身),即0,这样在运行结束的时候我们即能知道质数的个数。

法四:优化了法三标记合数的起点,法三从2x开始,实际上应该从xx开始标记合数。因为比x小的因数一定在之前就标记过了。比如74,这一看47是不存在的,因为4不是质数,但是4是合数,可变形为2*14,这就肯定标记过了

法五:线性筛\欧拉筛
按照法三的方法,45可以写成315或者59,他们显然是相等的,符合法三要求的在x*x之后,但是这属于冗余的操作

在逻辑上有很大的改动:
(1)单独维护一个数组,只存储已经筛出的素数
(2)标记合数要针对每一个数,只标记质数集合中每个元素与 x相乘的结果,在这里插入图片描述
保证了每个合数只会被其「最小的质因数」筛去,即每个合数被标记一次
在这里插入图片描述

说实话,这个流程一走下来,我立马就想到了DP,果然可以使用dp的思想

基于DP
法六:

3.8 各位相加

题目:给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。

分析:这是之前数字反转问题的进阶版,数字反转需要掌握每一位的推入和弹出。

官方给了一种魔法般地数学解释,只能说牛逼
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.9 丑数

题目:丑数 就是只包含质因数 2、3 和 5 的正整数。

给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。

分析:
在这里插入图片描述
首先,在大数运算上,将大数表示成幂的乘积的形式非常普遍,这是一个重要思路。另外,在程序上,n反复除以2,3,5是我一开始没想到的
while n % factor == 0:
n //= factor

四.栈

4.1 有效的括号

题目:给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

特殊情况:空字符串在题干要求下为真
在这里插入图片描述
观察字符串,左右括号是不会混乱排列的,最右边的左括号最先匹配,最左边的右括号最先匹配。后入先出的这种模式就可以使用栈这种数据结构
在这里插入图片描述

五.线性表和链表

前言

模式识别:需要移动左右两头的问题可以考虑双指针

5.1 指针

前言:双指针问题需要考虑清楚两个指针分别在什么情况下移动,怎样移动。fast指针会将每个元素都遍历到,因此就不要考虑slow指针是否也要判断元素,不需要,slow指针只表示位置

5.1.1 合并两个有序链表

题目:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

很容易想到,使用双指针法来解决,但不想线性表,链表要求不增加额外的存储空间

特殊情况:一个链表为空的情况

在处理链表问题时,总是增加一个头结点来统一插入、删除等操作,这样在处理边界的时候可以更方便,不需要额外的代码

这里需要自己定义链表结点这一数据机构

双指针法的核心思路是:永远只比较两个指针所指向的元素,结果符合要求的那个指针向后移动

5.1.1 合并两个有序数组

题目:给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

在解法上,双指针法已经非常熟悉了。针对本题,因为数组1预留了包函数组2的存储空间,所以采用从后往前扫描的双指针法可以不增加额外的存储空间

注意,双指针法那个指针符合条件,那个指针才移动

严格来说,上述反向进行的遍历需要3个指针

5.1.2 删除排序数组中的重复项

题目:给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

法一:
对于有序数组,如果允许额外存储,那么可以直接遍历数组,只通过相邻两个元素的比较结果进行操作,不同则记录,相同则跳过

但不允许额外存储的条件下呢?
在这里插入图片描述
设置快慢指针,慢指针负责指出可覆盖位置,快指针负责遍历数组
在这里插入图片描述
从下标为1的位置开始比较是因为,如果重复,至少是从下标为1的位置开始覆盖。另外,比较相邻元素取的是通过后一个元素与前一个元素比较,也就是fast 和 fast-1比较

同时,这样的设计背景下,慢指针是可以充当新数组的长度,也就是遍历原数组slow长度即是没有重复元素的新数组

特殊的,需要考虑数组长度为0的情况

5.1.2 删除排序链表中的重复元素

题目:存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。

返回同样按升序排列的结果链表。

相较于数组,删除重复元素之后需要覆盖,对于链表只需要重新链接即可,也不需要头结点来统一操作

不要怕链表的这些操作

5.1.3 移除元素

题目:给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

法一:
还是使用5.1.2的方法,由fast指针遍历数组,如果该元素不是目标,则写到slow的位置,slow和fast都向前移动
否则,fast向前移动

这一方法我觉得别扭的地方在于,假设有连续的元素都不是目标,但是还是需要写一遍。可以这样,判断fast和slow是否相等,实际上只要出现一次目标,fast和slow必发生错位

法二:
产生的背景是,如果要移除的元素恰好在数组的开头,可以直接将最后一个元素 55 移动到序列开头,取代元素 1
这个优化在序列中val 元素的数量较少时非常有效

此方法,双指针在一次循环中只有一个指针会发生移动,由left遍历数组,如果发现目标,right向左移动,left不动
否则,没发现目标,left向右移动

这样感觉就消除了前面非目标连续元素的问题

5.1.4 验证回文串

题目:给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

法一:反转字符串
在这里插入图片描述
字符串里面可能有空格,标点符号,他并不是简单地abba这种形式

使用两个指针分别指向串首和串尾,首先过滤掉不是数字、字母的字符,一旦出现了过滤的行为,两个指针会错位,剩下的过滤大小写会直接过滤掉该字符串

因为涉及到两个指针的移动,极端情况下指针会移动到串尾,因此每次移动完之后都需要判断指针位置是否符合条件

5.1.5 环形链表

题目:给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

解法:双指针法
利用快慢指针,慢指针一次走一步,快指针一次走两步
比较反直觉的一点是,快指针有可能会超过慢指针,但总会相遇在同一个位置

如果说不存在环,则快指针一定先到达链表尾部,要么本身为空,要么next为空

5.1.6 回文链表

分析:我选择最简单的一种方式,把节点的值取出来,就等价于处理回文数组了,经典的双指针法

实际上,可以直接使用快慢指针法,但是要麻烦许多
1.通过快慢指针的慢指针将链表后半段反转
2.判断是否回文
3.通过快指针恢复链表

5.1.7 删除链表中的节点

题目:请编写一个函数,用于 删除单链表中某个特定节点 。在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问 要被删除的节点 。

题目数据保证需要删除的节点 不是末尾节点 。

分析:可惜,没有注意到待删除节点不是末尾结点
先将待删除节点的val变成下一个节点的val,再删除下一个节点。链表节点的信息也就两个嘛,val和next

5.2 最后一个单词的长度

题目:给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。

单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。

反向遍历即可

因为题干已经说明,字符串至少含有一个单词,也就不需要考虑没有单词的情况

5.3 双指针

5.3.1 相交链表

题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

解法:双指针
在这里插入图片描述
双指针的更新策略非常巧妙,如果能让两个指针齐头并进,那么他们一定能同时到达相交点。但是两个单链表的长度不一样

但是,两个链表共享其中一段,只有交点前面一段不一样。如果说一个指针走过自己这一段,再走另一个指针走的那一段,那么他们一定会相遇在交点,因为总的来说,他们走过了相同的路程

5.3.2 反转链表

题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

解析:严格来说应该是3个指针,需要同时记录当前节点,前一节点,后一节点的地址,也就是链表操作中的“先连后断”, 连接之后再更新指针

还是加了一个为none的头结点,只是用来充当链尾。操作的顺序为:
(1)保存当前节点的下一节点
(2)当前节点指向前一节点
(3)指向前一节点的指针指向当前节点
(4)指向当前节点的指针指向下一节点

5.3.3 汇总区间

题目:给定一个无重复元素的有序整数数组 nums 。

返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表。

nums = [0,1,2,4,5,7]
[“0->2”,“4->5”,“7”]

分析:经典的双指针法。重点是找到high的位置。

和前面的双指针还有区别,并不是直接迭代low,high,而是迭代索引i,然后给low,high赋值

判断条件在题干中分析可以得出。low和high分别指向区间的左右。
在这里插入图片描述

5.3.4 会议室

题目:给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,请你判断一个人是否能够参加这里面的全部会议。

分析:核心在于判断区间是否有重叠。重点是要先进行排序,笑死

排序就是靠sort,sorted,都是快排

5.3.5 移动零

题目:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

分析:数组要进行元素的按值移动还是比较困难的。使用双指针法处理,要对两个指针的功能和操作定义清楚
left: 左边为非零数,一直指向已处理好的序列的尾部
right: 左边直至left为止,为0,一直指向待处理序列的头部

什么是处理好的??不含0
什么是待处理的??含0

总的来说,就是将left的0与right的非0进行交换,left找0,right找非0

这样说还是挺抽象的,具象一点:

  1. 遇到非0,两个指针均向前移动。很好理解,因为left没有找到0,所以就没有和right交换的需要
  2. 遇到0,right右移。这就说明,left找到了0,right需要去找下面第一个非0
  3. 经过情况2之后,如果right找到了非0,此时就应该交换left和right的值

我一直觉得别扭的地方在于,如果从一开始直接遇到情况1,不就意味着left和right都指向同一个元素吗??他们有必要进行交换吗??
A:这是一种必要的开销。只要进行过一次0的移动,left和right必然会错开。当然,如果从开头就是连续的非0,那么left和right的确会发生看起来没必要的交换

5.4 栈和队列

5.4.1 用队列实现栈

题目:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

分析:假设使用两个队列来模拟栈
入栈:队列1存储当前的栈内元素,并是指指针指向队首元素;队列2存储即将入栈的元素
将队列1的元素出队列1,入队列2,并交换两个队列以维持上述的定义,更新指针
其他操作基于队列1完成即可,主要就是通过两个队列实现入栈操作

5.4.3 用栈实现队列

分析:同样的序列分别推入栈和队列,弹出序列恰好相反,那么将栈的弹出序列再推入另一个栈,再弹出,不就和队列的弹出序列相同了吗

基于这一点,可以维护两个栈俩实现队列的效果。栈1负责弹出,栈2负责推入。
出栈:如果栈1不空,则弹出栈顶元素;如果栈1为空,将栈2元素全部弹出并推入栈1,再弹出栈顶元素

5.5 区域和检索 - 数组不可变

题目:给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。

实现 NumArray 类:

NumArray(int[] nums) 使用数组 nums 初始化对象
int sumRange(int i, int j) 返回数组 nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], … , nums[j]))

分析:当数组确定,使用函数多次求解区间和是没有必要的。每次求解区间和都需要遍历,在初始化的时候可以一次性求出阶梯和,阶梯和通过裁切可以得到区间和

阶梯和怎么求???
A:创建一个存储阶梯和的数组,先加入元素0,遍历原数组,每次选择一个元素和阶梯和数组的[-1]位相加

六.动态规划

6.1 最大子序列和

题目:给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

我的问题在于对状态方程的定义,我的定义是:
f(i)表示到第i个元素为止的数组的最大子序和,由此写出的状态转移方程是,f(i) = max( f[i-1], f[i-1] + nums[i] )

正确的定义是:
f(i) 代表以第 i 个数结尾的「连续子数组的最大和, 因此有
f(i)=max{f(i−1)+nums[i],nums[i]}

我的定义就不满足连续子序列的连续,对于第i个元素,只有两种状态,要么被包含进子序列,要么不被包含,自成一个序列。之所以只有两种状态,还是因为对f(i)的定义,必须要以第i个元素结尾

这也是我最迷惑的地方,子序列的位置是不固定的,这种不固定就导致有些反直觉。比如说对于数组[1,2,3,-4,5,6]。按照正确的定义,f(3)的最大子序和是2,但我总感觉应该是6

我做了这样一个尝试,将数组[1,2,3,-4]按照上述两种定义方式写出求解f(i)时涉及到的子序列,我发现,按照我的定义,f[i-1] + nums[i] 不一定成立,因为f(i-1)表示的最大子序列的最后一个元素不一定是nums[i-1]
按照正确的定义,f(i−1)+nums[i] 和 nums[i] 确实能够形成两个连续的序列
写出数组[1,2,3,-4]按照正确定义涉及到的连续子序列,我发现实际上它是包含了数组的所有连续子序列

核心就是要能够遍历所有的连续子序列
以[1,2,3,-4,4]为例:
dp[0]: [[], [1]]
dp[1]: [[2], [1,2]]
dp[2]: [[3], [2,3], [1,2,3]]
除了单独一个数字成立的连续子序列,dp[i]中其他的连续子序列之和通过dp[i-1]+nums[i]就可以计算出来,因为他们都是以nums[i]结尾的子序列

我想应该这样总结,通过计算所有以第i个元素结尾的连续序列,最终我们就计算了全部的连续序列,进而就可以求得其中和最大的连续序列

通过测试,不能将f(i-1)写入状态转移方程,他的值总是最大的

6.2 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

6.3 杨辉三角

题目:给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

生成杨辉三角的原理非常简单,只是说一般分析是写成等腰三角形,但写成程序,就写成直角三角形,以此来确定计算关系

6.4 杨辉三角2

思路同上,理解了如何生成杨辉三角即可

6.5 买卖股票的最佳时机

题目:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

解法:典型的动态规划,在这个问题上,注意,只进行一次交易
在这里插入图片描述
在这里插入图片描述
如果写出如同分析“最大子序和”那样的数对,感觉很难判断是否包含了所有的选择。在上面的分析中,对f(i)的定义实际上是前i-1个数中的最小值,当然,硬要说的话,将其定义为到第i个数的最大收益也不是不行。主要是要想清楚最大收益产生于什么时候

还是像前面最大子序和把排列写出来,本质上就是选长度为2的子序列
dp[2] = [7,1], max = 0
dp[3] = [7,1], [7,5], [1,5],max = 4
dp[4] = [7,1], [7,5], [1,5], [7,3], [1,3], [5,3]
如何完成过滤???
A:一部分是max过滤,一部分就是通过状态转移方程过滤

6.6 买卖股票的最佳时机2

题目:给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解析:这一次就能进行多次交易,但是交易不能重叠
收集所有上行区段的利润即可

6.7 比特位计数

题目:给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。

分析:
对于2的幂,已经知道可以通过x&(x-1)来判断。要计算i的二进制表示中1的个数,可以反复进行与运算。但是以前就知道 i 中1的个数和处于其之前的2的幂有关

那就可以使用DP的思想来处理,首先定义dp[i] 为数i的1的个数
实际上可以构造这样的状态转移方程:
dp[i] = dp[i - 当前最大2的幂] + dp[当前最大2的幂]
dp[2的幂] 都为1,因此可以将上式表示为:
dp[i] = dp[i - 当前最大2的幂] + 1

PS:这个所谓的当前最大2的幂,它基于这样一个背景,我们是从0到n进行遍历,也就是到i为止最大的那个2的幂的结果,比如说i = 4,那么当前最大2的幂就是4;同理,如果i = 3,则当前最大2的幂 = 2。从这里出发,需要做i - 当前最大2的幂的理由就充分了,以2的幂作为分界线

如果说i是2的幂,那么其二进制形式中1的个数就为1

七. 树

前言

先讲讲dfs,这个算法我接触的比较早,算法思想也符合我的思维方式。但是具体到代码层面,就和我的理解出现了偏差。
我的理解是,栈里面存的应该是一条可回溯的路径,比如说对于这样一张无向图
在这里插入图片描述
我认为最开始栈里边存的是A C D,从A开始按照规律向相连的最深处遍历,当D没有相邻边之后,回溯到C,看C还有没有未访问的相邻边,再回溯到A
很明显,对于每一个节点我们需要访问到相邻节点,如果暂时只取一个节点推入栈,那么应该以什么方式选择呢?等到回溯至此的时候,又以同样的方式选择一个节点推入栈吗?可想而知有多混乱

我想我产生这种想法的理由是,我总是认为栈里面应该存dfs的路径。但是是否可以这样,通过栈顶元素的弹出来确定dfs的路径呢?栈里面存储节点,每次遍历当前节点的所有相邻节点,推入没有访问过得节点,同时更新访问数组,记录已经访问的节点。所以栈里面没有重复元素,都是唯一的,这样也就保证了访问不会重复

为什么这种方法可以达到dfs的效果?通过上述方法,相当于每弹出一个节点,就会推入与之相邻的未访问节点。那么,弹出元素之间的关系就是层层向深处推进的关系,a[i-1]和a[i]相邻,a[i]和a[i+1]相邻

stack数组是用来达到dfs的核心,visit数组是辅助stack,保证不重复

树的遍历区别于图,我们对树的遍历的顺序做了规范,先序中序后序。
如果说使用dfs思想遍历二叉树,将节点推入stack默认顺序为先左子树再右子树,那么遍历顺序就等价于“根右左”

关于递归回溯的理解

7.1 树的遍历

7.1.1 二叉树的中序遍历

题目:给定一个二叉树的根节点 root ,返回它的 中序 遍历。

这是个老问题了,一般考虑使用递归,比较好理解
递归都可以改成迭代,也就是循环

两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,(一直朝左子树的最深处,到时候要回退)而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同

以回溯为重点考虑的话,是否可以这样理解
1.root不空,则一直寻找最深的左子树为空的节点
2.输出这个节点,将其作为新的根
3.如果root的右子树为空则回溯,即弹出栈顶元素如果root的右子树不为空,则重复上述过程

但是在代码层面并不会直接去判断root的右子树是否为空,核心代码会先找最深、且左子树为空的节点,然后将root更新为该节点的右节点

感觉这块有点乱,TNND

7.1.2 相同的树

题目:判断两棵树是否相同

既要进行数值比较,又要进行结构比较

乍一看,似乎个图算法用到bfs和dfs一样,但是两棵树就需要用到两个栈或者两个队列
dfs可以用递归来做,递归就需要递归出口和递归表达式
递归出口的范式一般是:如果…,返回…
如果两个根都为空则相同
如果其中一个为空则不同
如果两个的值不等则不同
数值和结构的比较都包含了

递归表达式基于树的这种分形结构,根比较完之后,就要确定左子树和右子树的情况

对于dfs的迭代版本,需要考虑循环条件,循环中何时退出。毫无疑问,只有整个迭代完成才能判断两棵树相同

7.1.3 对称二叉树

题目:给定一个二叉树,检查它是否是镜像对称的。

总结一下,先序中序后序遍历二叉树通常使用递归来实现,本质上使用的是dfs思想,递归隐式地维护了一个栈。之后“相同的树”这一题也是用递归解决,那么实际上递归通过不同的结构可以改变遍历的顺序

因此,熟悉递归的结构就尤为重要

递归出口:考虑高度为2的二叉树

  1. 根均为空,为真,甚至都不需要考虑高度影响
  2. 一个根为空,为假
  3. 两个根的值相同,树A的左子树和树B的右子树,树A的右子树和树B的左子树相等

这个问题和“相同的树”比较类似

7.1.4 二叉树的最大深度

题目:给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

还是可以沿用上述的dfs思想,如果我们知道了左子树和右子树的最大深度 l 和 r,那么该二叉树的最大深度即为max(l,r)+1,而左子树和右子树的最大深度又可以以同样的方式进行计算。因此我们可以用「深度优先搜索」的方法来计算二叉树的最大深度。具体而言,在计算当前二叉树的最大深度时,可以先递归计算出其左子树和右子树的最大深度,然后在 O(1) 时间内计算出当前二叉树的最大深度。递归在访问到空节点时退出。

递归调用的方法就是用于计算最大深度的方法

细品为何在root为空时返回0:明显,root为空,这一层不存在,当然返回0;但是root不为空,这一层肯定就是+1.所以总体来看它是一层一层进行的计算

7.1.5 将有序数组转换为二叉搜索树

题目:给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

法一:递归
首先是BST二叉搜索树的定义,从节点的值的角度出发,根比左子树的要大,比右子树的要小

之前我觉得困难的地方在于怎么控制子树的高度差,最后是通过默认的构造方式自动规避了这个问题,每次都将数组均分

7.1.6 平衡二叉树

题目:给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

解法:不是要构造平衡二叉树。平衡二叉树的定义是,任意结点的左右子树的高度差不超过1
仍然是用递归的方式,核心在于如何判断是否平衡

  1. 如果节点为空,则平衡
  2. 左右子树的高度差不超过1,且左子树,右子树也都是平衡二叉树

第二点不容易想到

递归的分支特别多,嵌套了很多层:
(1)计算二叉树的高度是递归
(2)计算每个根的左右子树是否是平衡二叉树也是递归
(3)(1)还是嵌套在(2)中

7.1.7 二叉树的最小深度

题目:给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点。

又是一个基于DFS,使用递归完成的问题

递归出口有3个:

  1. 根节点为空
  2. 根节点没有子树
  3. 根节点有子树,返回当前的最小深度 + 1

递归表达式为:
如果存在子树,则将其作为根,返回其高度和最小深度的比较结果,取更小的那个更新最小深度

PS:这个似乎没有好办法去剪枝,本来我是打算一旦碰到左右子树都为none那就终止,估计只有层序遍历才可以

7.1.8 路径总和

题目:
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。

叶子节点 是指没有子节点的节点。

解法:
还是一样,使用DFS的思想,用递归来完成
在这里插入图片描述
递归出口有两个:

  1. 如果根为空,返回FALSE。表示无法访问这个节点
  2. 如果该节点为叶子结点,进行值的比较

递归表达式:
如果根存在子树,那么需要看左子树或者右子树是否存在满足条件的情况

7.1.8 二叉树的所有路径

题目:给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

分析:老办法,DFS+递归实现。
在这里插入图片描述
递归出口要理解还是很容易的,但是在代码上有些我感觉反直觉的东西
在这里插入图片描述
就是说当开始遍历root的右子树的时候,为什么不会在路径上出现左子树的节点。在我的直觉上,我认为会出现左子树的节点???
A:其原因在于递归函数的参数,我们传递进去的当前额路径path。在上述代码中,else块的两个实参path是一样的,此时意味着在进行分叉,而分叉之前的路径是一样的。举个简单的例子,形如,
在这里插入图片描述
通过手动推算,开始访问root 1的左子树和右子树的path都是1->,然后进入递归

7.1.9 翻转二叉树

题目:翻转一棵二叉树。
在这里插入图片描述
感觉减少了,觉得题目描述都挺抽象的

分析:将一颗二叉树进行镜像翻转。二叉树问题普遍需要遍历,遍历通常又是用递归来实现

如果当前遍历到的节点root 的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以
root 为根节点的整棵子树的翻转

递归出口我总结下来,需要考虑最简单地情况:
1.根不存在
2.根存在
3.根的子树不存在子树了
如上,最多到高度为二的二叉树就能把出口描述清楚,感觉还是很微妙

之前有道题,相同二叉树,需要判断一棵树是否对称,思路类似

7.2 二叉搜索树的最近公共祖先

题目:给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]、

分析:
首先是二叉搜索树的定义,也叫二叉排序树。我们通过递归定义二叉排序树,先构造新的节点,若root为空,则直接插入;否则,若val小于根的val,则插入左子树,否则插入右子树

一次遍历真的很秀,很优雅
(1)p, q不存在咋办?
不可能。题干说了,树中指定的两个节点,找就完事了
(2)p,q在什么时候分叉?
首先,他们一定会分叉。根据bst的性质,分叉的节点一个比直接双亲大,一个小。这也就意味着,当root不能同时大于或者小于两点,则该root为最近的公共祖先。从此刻开始,两个节点分道扬镳

我们从根节点开始遍历;
如果当前节点的值大于p和q的值,说明p和q应该在当前节点的左子树,因此将当前节点移动到它的左子节点;
如果当前节点的值小于p和q的值,说明p和q应该在当前节点的右子树,因此将当前节点移动到它的右子节点;
如果当前节点的值不满足上述两条要求,那么说明当前节点就是「分岔点」。此时,p和q要么在当前节点的不同的子树中,要么其中一个就是当前节点。

7.3 最接近的二叉搜索树值

题目:给定一个不为空的二叉搜索树和一个目标值 target,请在该二叉搜索树中找到最接近目标值 target 的数值。

注意:

给定的目标值 target 是一个浮点数
题目保证在该二叉搜索树中只会存在一个最接近目标值的数

分析:
我最开始只有一点不确定,是否需要遍历所有节点????
A:不需要。通过数轴可以确定。如果target < root.val, 那么最接近的值一定在root的左子树。
反证法证明:如果在右子树,那么这个值一定大于root.val,而root.val才是最接近的值。矛盾

八. 位运算

8.1 只出现一次的数字

题目:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

利用了异或运算的交换律。异或运算,相同为0,不同为1,另外,a^a = 0
0 ^ a = a。0就相当于与乘法运算中的1

8.2 Excel表列名称

题目:给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称。

例如:

A -> 1
B -> 2
C -> 3

Z -> 26
AA -> 27
AB -> 28

解法:应该想到他的模式,每26位要进一位。进一步想到考察进位机制
本质上就是10进制转26进制,那就是 :除进制取余倒排

有点反直觉的地方在于实现数字和字符串的转换,其实使用纯数字来表示可能更好理解,这也就意味着莫以为可以是(26),所以某个数字就成了
(26)(26),由此看看怎么实现转换

有点麻烦的是这个0,不管是10进制还是二进制都有0,10进制转二进制,余数为0是不用特殊处理的。

8.3 颠倒二进制位

题目:颠倒给定的 32 位无符号整数的二进制位。

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。

解析:输入是一个二进制数,只是要注意一点,当他作为二进制数时,高位的0会被省去,比如000111,在实际使用的时候就变成111

8.4 2的幂

题目:给你一个整数 n,请你判断该整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false 。

如果存在一个整数 x 使得 n == 2x ,则认为 n 是 2 的幂次方。

分析:
我一直对位运算不熟悉,没怎么用过。之前只是用过运算的相关性质,0相当于乘法中的1
n & (n - 1)用于移除最低位的1
实际上只有两种情况:(1)不需要借位时,最低位的1必然是在末位,那么n和n-1的其他位相同,与运算之后n的其他位不变,末位变0;(2)需要借位时,必然从最低位的1借出,那么从最低位的1开始直至末尾,n和n-1不同,与运算之后,这些为全为0,也移除了最低位的1

在本题中,2的幂的二进制表示必然只有一个1,那么移除最低位的1之后此数必然变为0

8.4 3的幂

没有办法像求解2的幂那样直接使用位运算,但是x的幂的特点是,(1)首先是正整数 (2)一直除3都能整除

8.5 丢失的数字

题目:给定一个包含 [0, n] 中 n 个数的数组 nums ,找出 [0, n] 这个范围内没有出现在数组中的那个数。

分析:首先是这个题目看得人有点懵逼。[0,n]有n+1个数字,但是只包含了n个,范围在[0,n+1],找出缺的那一个

我选择了位运算的解法,比较喜欢异或运算,核心就这两个:x^x = 0
x^0 = x
如果在原序列后面再补上[0,n+1]这n+1个数,依次进行异或,因为缺失的数字只出现了一次,那么最后的结果就是这个确实的数字

巧妙地地方在于,并不需要真的往数组补上n+1个数,因为数字范围和下标是一致的,因此让数组下标参与异或即可,最后再补上len(nums)即可。真的非常优雅
在这里插入图片描述

九. 字符串

9.1 最短单词距离

题目:给定一个单词列表和两个单词 word1 和 word2,返回列表中这两个单词之间的最短距离。

分析:
我最初的想法是,先找第一个,再找第二个,然后计算距离,但是处理不了这种情况
a…a…b…b
此问题的关键在于,必须要在遍历的过程中同时找两个目标,实时更新他们的位置

如上例,前面有重复的单词1,一直没有找到单词2,那么处于后位的单词1肯定离单词2更近

十. 博弈论

10.1 nim游戏

题目:你和你的朋友,两个人一起玩 Nim 游戏:

桌子上有一堆石头。
你们轮流进行自己的回合,你作为先手。
每一回合,轮到的人拿掉 1 - 3 块石头。
拿掉最后一块石头的人就是获胜者。
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false 。

分析:
在这里插入图片描述
推广开来,将整个游戏进程视作k个回合,每回合各抓一次。
后手要赢就得保证在最后一个回合开始前石子总数是4的倍数。
先手要赢就得破坏这个条件,在先手抓后,使总的石子数变成4的倍数。也就是把超过4n的那部分抓掉

进一步讲,从一开始结局就是肯定的

当规则形成,游戏中只有最精明的玩家时,结局是注定的

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值