算法面试高频算法题(leetcode)详解 python3.7实现 最暴力、最佳的解题方法

哔哩哔哩数据结构配套的全部代码课件链接(点这里)

链接:https://pan.baidu.com/s/1DbNXI1W_i8gni1Fqz6CWbQ
提取码:p7mu

面试高频算法习题精讲

  • 如果你没有数据结构与算法基础:我给你推荐清华大学博士授课的python数据结构 绝对讲的好
    课程链接:https://www.bilibili.com/video/av74879701
  • 有了基础直接先刷下面的几十道最常考的算法面试题。
  • 最后配合剑指offer这本书足够了(下个月我会把剑指offer整理好)

课程亮点

  • 从实际题目入手,满足用户突击刷题需求
  • 对题目进行深度剖析,帮助优化解题思路
  • 代码实操详解题目,总结题目解决规律
  • python语言实现
  • 从高时间高空间的暴力解法到深入掌握最优方法
  • 收获适合自己的解题方法与解题思路

课程目录

第1章 准备工作

01 开篇词:这个专栏能给你带来什么?
  • 每道题都有不同的思路讲解,从最暴力的时间空间复杂度双高的方法开始优化,一步步降低
    时间空间复杂度;
  • 语言代码一手抓:Python
  • 题目选自当下最受面试官欢迎的面试题目网站 LeetCode,增加你的面试成功率,即使你工
    作了也可以不时来刷一刷,保持自己的水平;
02 online judge 的原理
  • 什么是 online judge ?online judge(在线评判功能)。
    在前面的介绍中我们已经知道本专栏是一本基于 LeetCode 算法题库的网站,而online judge
    就是网站对于个人所提交的代码的评判功能,这一小节我们来简单认识一下 online judge 的原
    理。
    在开始之前请思考以下问题:
    问题一:leetcode中,提交的代码能否改变给定的类名和方法签名?
    问题二:leetcode中,进入方法后,执行逻辑前必须重新初始化实例变量,这是为什么?
    问题三:为什么问题的样例我过了,提交代码后一直显示wrong answer?
  • 了解 online judge 原理的意义
    正如上面那三个问题,跟算法毫无关系。但是,我们了解 online judge 原理后,可以规避一些
    由于不了解判题系统而导致的低级错误,减少错误提交。
    online judge 的组成
    一般分为两个部分:
  • web服务:用来展示题目,接收用户提交的代码,并把结果展示给用户;
  • 判题系统:将用户提交记录进入队列,依次在沙箱中判题,并把结果保存在数据库。
    要知道题目的种类是很多的,而且类似 LeetCode 的刷题网站有很多。对于各种类型的题目,
    他们的 online judge 也都各有差别,我在这里大致的将题目分为三种:
  • 用户提交的程序必须包含输入输出部分,输出唯一;
  • 包含输入输出部分,但输出不唯一;
  • 给定一个函数定义,在函数中实现逻辑。
    下面我们来看下各种刷题网站的 online judge 是怎么评判对错的:
    牛客网:用户提交的程序必须包含输入输出部分,输出唯一。
    牛客网的 online judge 会预先把测试数据准备好,分为输入文件和正确答案的输出文件。当我
    们提交代码后,online judge 会自动将我们的代码保存成文件然后进行编译。如果在编译的过
    程中遇到错误,那么会直接出现compile error。
    在编译完成之后判题系统执行我们的代码,并且读取输入文件,将我们的输出定向到输出文件,
    同时设置超时时间。

将 code.py 文件的输出定向到 ouput 文件中
简单的linux例子:cat input | python code.py > output
如果代码执行时间超出指定时间,会返回 time limit exceed(时限超出)。代码执行完成之后,
判题系统将得到的输出文件与正确答案进行比对,如果输出文件与正确答案相同则返回
accepted 。如果输出文件与正确答案比对有差别则返回 wrong answer(错误的答案) 或者
output limie exceed(输出太多信息)。
codeforce:包含输入输出部分,但输出不唯一。
这类题目很有趣,举个例子:

  • 给定一个正整数N,输出所有符合a+b=N(a和b都是正整数)的ab对

这类题目没有固定的答案,100个人可能有100种答案。这类题目与输出唯一题目不同的是:这
种题目在判题的时候不是对比输出文件和正确答案。网站会预先准备一套判题代码,判题代码会读取输出文件和正确答案,然后通过特有的逻辑判断答案的对错。
LeetCode:给定一个函数定义,在函数中实现逻辑。
对于这种类型的题目 LeetCode 会针对每一道题目写一个测试代码。你输入什么,测试代码对
比输出什么。如果答案不唯一还会根据题目信息写一个校验代码。
判题系统会将我们实现的函数代码进行编译,然后与测试代码放在一起。测试代码调用我们编写
的函数进行测试,并且设置超出时间。当我们编写的代码输出结果后,由校验代码判断是否通
过。

03 时间复杂度与空间复杂度分析

在各种类型的刷题网站中,大部分题目要求答案代码执行时间都是1秒钟。所以我们在做题之前
需要优先考虑我们即将要实现的算法能否在 1 秒钟之内完成,这就要求我们了解 1 秒钟大概能
做什么量级的运算。在特定的数据范围内我们的算法会不会超时,所以对算法的时间复杂度预估
就非常重要。

时间复杂度
说到排序,我们的脑子里总会出现几种耳熟能详的排序算法,如插入排序、冒泡排序,快速排序
和归并排序。其中插入排序、冒泡排序的时间复杂度是o(n^2),快速排序、归并排序的时间复
杂度是o(nlogn)。那么这个时间复杂度是什么意思呢?
大家都知道,计算机执行程序是通过二进制指令来执行的,二进制数据最基本的操作有——与
(&)、或(|)、非()、异或(),基本四则运算(+、-、*、/),逻辑运算(>、<、=)是
转换成有限次二进制指令来执行,次数不跟参与计算的数据大小有关,我们称之为o(1)复杂度。
大家请看如下代码:

int sum = 0;
	for (int i = 0;i < N;i++) {
		for (int j = 0;j < N;j++) {
			sum++;
		}
	}

这段代码中, int sum = 0 执行了一次, int i = 0 执行了一次, i < N 执行了N次, i++ 执行了 N 次, int j = 0 执行了N次, j < N 、j++ 和sum++ 分别执行了N^2次,我们假设赋值、时间复杂度关注的是影响速度的因素。上述式子,当 N 增大,N^2 比 N 大得多,在 N^2 面前,N 的影响几乎可以忽略,因此我们忽略掉 N 的部分,将上述代码的时间复杂度简写为o(N^2)。
1秒钟

  • 1秒钟能执行千万级别的运算,千万级别是什么概念呢?
  • n取值在百万-千万左右,o(n)的算法能在1秒钟内实现
  • n取值在十万左右,o(nlogn)的算法能在1秒钟内实现
  • n取值在一千左右,o(n^2)的算法能在1秒钟内实现
    ……
    上面我们看了一个简单小案例的时间复杂度分析,和各种时间复杂度下1 秒钟能够完成多大量级
    的运算。如果你还是觉得不明白也没关系,在后面的专栏中也会慢慢教你如何分析时间复杂度。
    下面我们来看下空间复杂度。
    空间复杂度
    内存中基本单位是字节,1个字节为8位,总共可以表示2^8=256个值。

我们在设定变量时,一旦变量赋值成功,该变量就在内存中有一片指向的地址,这片地址是操作
系统分配的。该变量定义的时候需声明需要多少空间,操作系统会给分配多少空间。一旦分配,
这个变量在内存中就固定下来了。

我们在代码中需要声明每个变量的类型,比如int,为4个字节的整型;double,为8个字节的浮
点型;char,为一个字节的字符等等。python由于可以不用声明类型,我们很少关注每个变量
所占用的内存空间。python底层仍然是存在基本类型,只是封装了一层动态识别。

基本类型占用空间表示数的个数
char1个字节256
byte1个字节256
bool1个字节256(实际上只用了两个值)
short2个字节65536
int4个字节4294967296
double8个字节11位整数,52位尾数,1位符号
  • 空间复杂度也有跟时间复杂度一样的表示方式。
    一个n个元素的数组,是o(n)的空间复杂度;一个n*n的二维数组,是o(n^2)的空间复杂度。这
    里的o(n)表示,存在一个跟n无关的常数k,使得申请的内存空间为 kn 。
    例子:(PYTHON)

a = [0] * N; # 空间复杂度o(N),但申请的内存空间不止4N个字节,python为数组封装了一层,里面还会记录一些常用的元数据以便快速查询,比如size

写在最后
不管是刷题,还是在平时工作中写代码,我们对自己写出来的代码的时间复杂度和空间复杂度应
当有所理解,对代码消耗的内存和花费的时间有个底。时间复杂度和空间复杂度都需要根据场景
变化而变化。比如响应速度快的,通常用空间换时间的原则;内存放不下,就适当放宽时间复杂
度来换内存的优化。设计代码就是对时间、空间、开发成本各种要素的平衡,尽可能取到最优化
的策略。

第2章 初级难度试水

04 两数之和

刷题内容

难度: Easy
题目链接:https://leetcode-cn.com/problems/two-sum/。

  • 题目描述

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

题目详解
这道题目给我们一个数组,数组里面全是整数,然后再给我们一个数字target ,需要我们求出
在这个数组中哪两个数字的和正好是target 。注意,你不能重复利用这个数组中同样的元素,
指的是每一个位置上的数字只能被使用一次,比如数组 [3,3,5],你可以使用第一个和第二个
3,但是你不能使用第一个 3 两次。
暴力解法,双重循环遍历
外层循环从数组中取出下标为 i 的元素 num[i] ,内层循环取出 i 之后的元素 nums[j] 一一与
下标为 i 的元素进行相加操作,判断结果是否为 target 。

  • 为什么内层循环要取i 之后的元素的元素呢?因为如果第二轮取得i 之前的数的话,其实我们
    之前就已经考虑过这种情况了(即外层循环已经遍历过此时内层循环的这个数字)。

题目只要求找到一种,所以一旦找到直接返回。时间复杂度中的 N 代表的是 nums 列表的长
度。

下面我们来看代码:
Python beats 27.6%

class Solution(object):
	def twoSum(self, nums, target):
	"""
	:type nums: List[int]
	:type target: int
	:rtype: List[int]
	"""
	# 第一轮遍历
	for i in range(len(nums)):
	# 第二轮遍历不能重复计算了
		for j in range(i+1, len(nums)):
			if nums[i] + nums[j] == target:
			# 注意 leetcode 中要求返回的是索引位置
			return [i, j]

思路2:时间复杂度: O(N) 空间复杂度: O(N)
上面的思路1时间复杂度太高了,是典型的加快时间的方法,这样做是不可取的。其实我们可以
牺牲空间来换取时间。

我们希望,在我们顺序遍历取得一个数 num1 的时候,就知道和它配对的数 target-num1 是
否在我们的 nums 里面,并且不单单只存在一个。比如说target 为4 , nums 为[2,3] ,假设
我们此时取得的num1 为2 ,那么和它配对的2 确实在nums 中,但是数字2 在nums 中只出
现了一次,我们无法取得两次,所以也是不行的。

因此我们有了下面的步骤:

  1. 建立字典 lookup 存放第一个数字,并存放该数字的 index ;
  2. 判断 lookup 中是否存在 target - 当前数字cur , 则当前值cur 和某个lookup 中的key 值相
    加之和为 target ;
  3. 如果存在,则返回: target - 当前数字cur 的 index 与 当前值 cur 的 index ;
  4. 如果不存在则将当前数字 cur 为key,当前数字的 index 为值存入 lookup 。
    下面我们来看下代码:
    Python beats 100%
class Solution(object):
	def twoSum(self, nums, target):
	"""
	:type nums: List[int]
	:type target: int
	:rtype: List[int]
	lookup = {}
	for i, num in enumerate(nums):
		if target - num in lookup:
			return [lookup[target-num], i]
		else: # 每一轮都存下当前num和其index到map中
			lookup[num] = i
	

就像之前提到的特殊情况一样,这里注意我们要边遍历边将 num: idx 放入 lookup 中,而不是
在做遍历操作之前就将所有内容放入 lookup 中。例如数组 [3,5],target = 6,如果我们一次
性全部放进了 lookup 里面,那么当我们遍历到3的时候,我们会发现 target - num = 6-3 = 3
在 lookup 中,但其实我们只有一个 3 在数组中,而我们使用了两次,误以为我们有2个3。

class Solution:
	def twoSum(self, nums, target):
	"""
	:type nums: List[int]
	:type target: int
	:rtype: List[int]
	"""
	lookup = {}
	for i, num in enumerate(nums):
		lookup[num] = i
		for i, num in enumerate(nums):
			if target - num in lookup:
				return [i, lookup[target-num]]

Wrong Answer
Input
[3,2,4]
6
Output
[0,0]
Expected
[1,2]

总结

  • 拿到题目之后我们可以用最朴素和最暴力的方式来解答。这样做虽然没有什么问题,但我希
    望你在用最简单的方式去解答完成之后,多想想怎么可以去优化一下你的解答方式,培养你
    的“最优”思维。习惯这样想之后你的代码才会越来越优秀;
  • 当然。不是任何时候“最优”即最合适,我们也要考虑一下条件限制的问题,在各种环境中
    找出最合适的方式才是真正的“最优”;
  • 常见的减小时间复杂度的方式有:用空间来弥补多余的计算。
05 整数反转

刷题内容

难度: Easy
原题连接:https://leetcode-cn.com/problems/reverse-integer/。

内容描述

给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
示例 1:
输入: 123
输出: 321
示例 2:
输入: -123
输出: -321
示例 3:
输入: 120
输出: 21
注意:假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−231, 231 − 1]。请根据这个假设,如果反转后整数溢出那么就返回 0。

题目详解
这道题中我们要注意以下几点:

  • 整数会有负数的情况;
  • 题目中要求的有符号整数是一个 32 位整数(32 位整数的取值范
    围:-2147483648~2147483647,超出此范围即为溢出),如果将整数反转后发生了溢出
    情况,那么要返回 0 ;
  • 最后一位是 0 的情况下要舍弃,例如 120 反转后为 21 。
    解题方案
    思路1:时间复杂度: O(lgx) 空间复杂度: O(1)
    翻转数字问题需要注意的就是溢出问题,为什么会存在溢出问题呢?

我们int 型的数值范围是 -2147483648~2147483647 (-2^31 ~ 2^31 - 1) , 那么如果我们要
翻转 1000000009 这个在数值范围内的数,得到 9000000001 ,但翻转后的数就超过了范
围,这个情况就是溢出,这个时候程序返回 0 即可。

如果输入的是负数,就递归调用原函数,参数变成 -x 即可,代码大致执行流程如下:

  • 首先判断 x 是否为负数,如果是负数先取绝对值然后递归取反,最后再将结果转换为负数即
    可;
  • res 为最后结果,最开始等于 0 。x % 10 (例如: 123 % 10 = 3 ) 取出最后一位数。每次得
    到最后一位数字,并将其作为结果中的当前最高位;
  • 判断结果 res 是否溢出,如果溢出则返回 0 。

下面来看详细代码:
Python beats 69.76%

class Solution:
	def reverse(self, x: int) -> int:
		if x < 0: # 判断是否为负数
			return -self.reverse(-x) # 如果是负数则取绝对值调用自身,最后将结果转为负数
		res = 0
		while x: # 每次得到最后一位数字,并将其作为结果中的当前最高位
			res = res * 10 + x % 10
			x //= 10
		return res if res <= 0x7fffffff else 0 # 如果溢出就返回0

小结

  • 时刻注意特殊情况——为负数怎么办;为0怎么办;为空怎么办。
  • 合理运用写完的函数,机智一点,不要碰到负数又去实现一个负数的版本,直接递归调用原
    函数就可以了。
06 回文数
07 整数转罗马数字&罗马数字转整数
08 最长公共前缀
09 有效的括号
10 合并两个有序链表
11 删除排序数组中的重复项
12 移除数组中的指定元素
13 实现 strStr() 函数

第3章 中级难度涨经验

14 两数相加
15 无重复字符的最长子串
16 最长回文子串
17 Z 字形变换
18 字符串转换整数 (atoi)
19 三数之和
20 最接近的三数之和
21 电话号码的字母组合
22 删除链表的倒数第 n 个节点
23 括号生成
24 两两交换链表中的节点
25 盛水最多的容器
26 下一个排列
27 搜索旋转排序数组
28 摆动排序

第4章 高级难度超越自我

29 寻找两个有序数组的中位数
30 正则表达式匹配
31 合并K个排序链表
32 K 个一组翻转链表
33 单词接龙
34 寻找最近的回文数
35 地下城游戏
36 学生出勤记录
37 和最少为K的最短子数组
38 猫和老鼠

第5章 专题部分

39 专题1:LRU Cache 最近最少使用算法
40 专题2:二叉树遍历
41 专题3:二分算法
42 专题4:斐波那契数列
  • 斐波那契数,原题链接 跟剑指offer10题一样
  • 通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1. 给定 N,计算
F(N)。

示例 1: 输入:2 输出:1 解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2: 输入:3 输出:2 解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3: 输入:4 输出:3 解释:F(4) = F(3) + F(2) = 2 + 1 = 3.

提示:0 ≤ N ≤ 30

递归方案 浪费时间就不写了

法案一

class Solution(object):
    def fib(self, N):
        """
        :type N: int
        :rtype: int
        """
        F = [0,1]
        for i in range(2, N+1):
            F.append(F[-1] + F[-2])
        return F[N]

方案2

class Solution(object):
    def fib(self, N):
        """
        :type N: int
        :rtype: int
        """
        if N==1:
            return 1
        if N==0:
            return 0
        a,b=0,1
        for i in range(2, N+1):
            a, b = b,a+b
        return b

2个方案的时间、空间复杂度几乎一样。
类似的一系列问题可以解决

  1. 青蛙跳台阶
  2. 铺砖
  3. 爬楼梯
  4. 第 N 个泰波那契数
    参考剑指offer 79页
43 专题5:大整数
44 总结:写在最后
  • 5
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值