算法的自我修养(Python)
Algorithm
目录
[TOC]
一些感想
- 目前学到的算法感觉都是有一个优化过程:暴力遍历 —> 优化思路 —> 优化查找 —> 优化数据结构 —> 优化语法 —> Finish!
素数算法
欧拉筛法
1 | def OlaPrimeCalc(n): |
埃氏筛法
1 | def eladuosai(n): |
正整数拆分算法
简洁递归法
分解的正整数超出100,时间即大幅增加。
1 | def f(n,a): |
输出情况法
分解的正整数超出80,时间即大幅增加。
1 | import sys |
母函数法
1 |
五边形数定理法
1 |
递归算法
例一:台阶走法问题(一次只能走1或2步,到n阶有多少种走法)
最后一步、倒推
1、n=0 和 n=1 时,可以得出f(0)=0、f(1)=1; 2、n>=2时情况变复杂,但是最后一步只有2种情况:走1步(n-1)、走2步(n-2)。所以可以得到递推公式:
f(n)=f(n-1)+f(n-2); 3、使用递归或迭代。
动态规划
01相等连续子串(前缀和)
说明:给定一个0、1组成的字符串,请找到一个尽可能长的连续子串,其中包含的0与1的个数相等。
将0视作-1,1视作1,利用前缀和。
那么一对(0, 1)
之和为0
,满足‘0’、‘1’个数相同的子串首尾前缀和相同。(首:开头前一个;尾:最后一个)
例如:
索引:0 1 2 3 4
数值:0 0 1 1 0
sum:-1 -2 -1 0 -1
思路:
使用字典1记录某一个前缀和第一次出现的索引
临时变量1依次记录前缀和
字典2记录相等前缀和的后一个索引
临时变量2记录相等前缀和的最大长度
1 | n = int(input()) |
两数之和(哈希应用)
题目
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
1 | 给定 nums = [2, 7, 11, 15], target = 9 |
解答
- 一般想法:遍历所有可能,二重循环,时间复杂度是\(O(n^2)\),空间复杂度是\(O(1)\)。
- 高级点的想法:由于两数之和一定,有无效的遍历,可以减小解空间。先对目标数组进行排序(方法自选),再进行二分查找。这样做的缺点是算法不稳定: 1、对于特殊构造的数组会花费大量时间排序; 2、答案为相同元素相加时,返回原数组寻找下标很麻烦; 3、需要对二分查找进行优化,使其能够在无法找到的情况下收敛。 时间复杂度约为\(O(n\log_2{n})\)。
- 遍历的瓶颈在于查找时间长,那么使用哈希表为查找提速(两次使用哈希函数,第一次添加,第二次查询)
- 使用一次哈希函数,边添加边查找。
一份哈希函数
1 | class Solution: |
Q:这里就有问题了:如果数组中存在两个同样的数字x
,target
为这两个数字之和。这样构造的哈希表存储的x
的下标就是后面那个(前面的x
下标被覆盖),信息被筛选了,这样得出的解答合理吗?
A:根据题目要求,target
则必为这两个数字之和,且组合方式唯一。
在两份哈希函数的解法中,第一次遍历是为了获取哈希表,第二次遍历的是原数组,因此仍然能够获取到哈希表中被覆盖的那个下标,并得出正确解答。
在一份哈希函数解法中,依然先遍历原数组,后添加字典,信息可以在筛选前发挥用处。
分析
思路是优化二重循环的过程:
优化首先要找瓶颈,代码时间上的瓶颈就是遍历两次,一次遍历目标(很难优化哦),一次查找(😍,可以)。
二重循环暴力破解 —> 排序 + 二分查找 —> 遍历 + 哈希函数提速查找 —> 哈希函数结合遍历
还可以用首尾递进查找,时间复杂度\(O(n\log_2{n})\)
各有优缺点:哈希函数用空间换时间(Python版本空间消耗尤其明显)。在如今不怕内存小的环境下,也许能够为大家接受吧。
查找优化看hash
整数反转(取数位)
题目
给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
1 | 示例 1: |
注意:
假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [\(−2^{31}\), \(2^{31} − 1\)]。请根据这个假设,如果反转后整数溢出那么就返回 0。
解答
- 对于Python这种支持超长整数的语言来说,这里的溢出是不存在的,所以这道题对Python来说有些鸡肋。
- 不考虑语言,从效率入手,首先想到的就是通过
%10
获取数位,正负整数可以一起判断。 - 至于
32 bits
整数溢出问题,这里需要采用64 bits
的整数,通过不等判断来判断是否溢出。
Python字符串方法(非算法)
1 | class Solution: |
Python数值方法
1 | class Solution: |
C数值方法
1 | int reverse(int x){ |
\(2^n = 1 << n\),位运算,表示\(1*2^n\);相反,\(m >> n\)表示\(m/2^n\)
分析
C语言中,取模运算分离了整数的最后一位,并且带上了原数的符号。
Python中,取模运算使得商尽可能小,而C语言中,取模运算使得商尽可能大,。这就导致了余数的不同。
反转数字的算法:用一个数来累计反转后的数字,每次*10
提高数位,再加上原数分离的数位;原数不断通过%10
和/10
分离数位,更新自身。
斐波那契数列生成第n项
规定第0项为0,第一项为1.
初学者一般使用递归方法,简单明了:
1 | def fab(N: int) -> int: |
缺点
递归耗时间,每一个中间的fab(k)
都需要再递归求解,时间复杂度是可怕的\(O(2^n)\),笔记本求解第45-50项时已经需要耗费大量时间了。
优化
我们把计算过的fab(k)
储存起来怎么样?这样就不用再次求中间的数了。
因此可以使用数组储存斐波那契数列。
但是这样只能求解有限项数的fab(k)
,所以想到了动态内存,但是一旦项数太大,内存也装不下这么多数据。
再次优化
数组方式极大减少了计算量,但是我们计算指定项数的斐波那契数列时,只用到了“两项”:fab(k-1)
,fab(k-2)
,因此只需使用两个中间变量即可:
1 | def fab(N: int) -> int: |
当然,可以使用Python一些语法来加速运算,这里力求明确显示出算法的过程
当从0项开始时,循环次数是
N-1
,从1项开始时,循环次数是N-2
二维数组中的查找
题目
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
- 每行的元素从左到右升序排列。
- 每列的元素从上到下升序排列。
解答
我的初始思路是:比较每行第一个值确定行范围,再比较每行最后一个值筛选行,并遍历符合条件的行。
但是这样对于规模十分大的方阵来说,算法的最坏情况是\(O(n^2)\),即全部遍历一遍,即使对于小规模矩阵,也很可能遍历一半以上。
优化
我们希望减少遍历查找的规模,如果能确定遍历的方向(纵横)那就再好不过了。
观察矩阵,从左上角元素开始,比它大的方向有两个,这就导致了初始思路需要多次比较确定范围;从右下角元素开始,比它小的方向也有两个,同样难以减少规模。
但是呢,从矩阵右上角和左下角的元素开始,比它们大和小的方向各只有一个,这样就可以确定便利的方向啦。
算法描述
- 指针指向矩阵右上角元素;
- 大于
target
,指针左移一格; - 小于
target
,指针下移一个; - 等于
target
,返回指针位置; - 重复2、3、4步。
1 | def MatrixSearch(target: int, nums: List[int][int]) -> Tuple[int]: |
这里假设一定有解,如果可能没有解,需要加终止条件。
移除元素
题目
给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
1 | 给定 nums = [3,2,2,3], val = 3, |
示例 2:
1 | 给定 nums = [0,1,2,2,3,0,4,2], val = 2, |
解答
双指针法(首尾指针)
1 | class Solution: |
上述代码由于判断分支(if else
)比较多,效率不太高。
不过优化它的办法不属于算法范围,故此处不加讨论,只显示代码:
1 | class Solution: |
emmm,Leetcode上显示22ms,然而我提交时只有44ms,也许是机器问题吧。。。
不过真是。。。思路简单清晰,代码好写。
==所以,计算机运行if else
的效率到底如何呢?==
要点
注意到,第一块代码从末端遍历,第一个指针指向i = -1
,这种设计能够让输入空数组或者全是弹出数的数组时能够正常运行。
==思考==
双指针法(一端)
1 | // 官方解法,在数组小时有不必要的交换操作 |
这代码,简洁,有效率,容易理解。。。。双指针好优雅