系列文章目录
Leetcode刷题:热题HOT100-Medium篇-Python多算法实现(1-10题更新完毕)
文章目录
- 系列文章目录
- 前言
- 1. 两数之和(哈希表,数组)
- 2. 有效的括号(栈)
- 3. 合并两个有序链表(链表)
- 4.爬楼梯(动态规划)
- 5.二叉树的中序遍历(二叉树)
- 6. 对称二叉树(二叉树)
- 7.二叉树的最大深度(二叉树)
- 8.买卖股票的最佳时机(动态规划)
- 9.只出现一次的数字(异或运算)
- 10.环形链表(链表)
- 11.相交链表(链表)
- 12.多数元素(哈希,分治,计数)
- 13.反转链表(链表)
- 14.翻转二叉树(二叉树)
- 15.回文链表(链表)
- 16.移动零(数组)
- 17.比特位计数(动态规划,位运算)
- 18.找到所有数组中消失的数字(哈希表)
- 19.汉明距离(位运算)
- 20.二叉树的直径(二叉树)
- 21.合并二叉树(二叉树)
前言
记录LeetCode 热题 HOT 100的easy题目题解,采用python实现。
1. 两数之和(哈希表,数组)
1.1 题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
1.2 核心思路
该题目可以通过暴力枚举的方法计算出满足nums[i]+nums[j]=target的i和j,但是时间复杂度过高,因此采用哈希表的方法实现。
- 遍历nums数组中的每一个元素,若target-nums[i]不存在于哈希表中,则将num[i]作为key,i作为value插入哈希表。【可以避免nums中相同元素相加为target】
- 时间复杂度:由于哈希表查询的时间复杂度为O(1),因此,题目的复杂度取决于遍历nums数组,所以未O(n)
1.3 代码
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashmap={}
#查询target-nums[i]是否存在于hashmap中,若不存在,则将nums[i]插入hashmap
for i in range(len(nums)):
if target-nums[i] in hashmap:
return [i,hashmap[target-nums[i]]]
hashmap[nums[i]]=i
2. 有效的括号(栈)
2.1 题目描述
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
每个右括号都有一个对应的相同类型的左括号。
示例:
输入:s = "()"
输出:true
输入:s = "(]"
输出:false
2.2 核心思路
该题目可以通过栈数据结构实现括号的匹配。
首先,遍历s字符串:
- 当s[i]为左括号时,将s[i]压栈
- 当s[i]为右括号时,分为两种情况:栈为空或栈不为空。若栈为空,则栈顶元素无法弹出于s[i]匹配,返回False;若栈不为空,则弹出栈顶元素于s[i]匹配。当栈顶元素与s[i]匹配时,则继续白能力字符串中下一个元素;当栈顶元素和s[i]不匹配时,则返回False
其次,对于括号的匹配,采用字典数据结构实现。设置字典
bracketsMatch={"(":1,"[":2,"{":3,"}":4,"]":5,")":6}
当栈顶元素在字典中的值和s[i]元素在字典中的值相加为7时,则括号正确匹配;反之,则括号匹配失败。同时,为了缩短代码执行的时间,左括号压栈的判断时,采用bracketsMatch[s[i]]<=3实现。
- 时间复杂度:由于压栈和弹栈操作的时间复杂度均为O(1),因此本题的时间复杂度取决于对字符串的遍历,所以未O(n)
2.3 代码
class Solution:
def isValid(self, s: str) -> bool:
stack=[]
bracketsMatch={"(":1,"[":2,"{":3,"}":4,"]":5,")":6}
for i in range(len(s)):
#若为左括号,则入栈
if bracketsMatch[s[i]]<=3:
stack.append(s[i])
else:#若为右括号
#首先,若栈此时为空,则return false
if len(stack)==0:
return False
else:
if bracketsMatch[s[i]]+bracketsMatch[stack.pop()]!=7:
return False
return True if len(stack)==0 else False
3. 合并两个有序链表(链表)
3.1 题目描述
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
输入:l1 = [], l2 = [0]
输出:[0]
3.2 核心思路
题目已知两个链表按非递减顺序排列,因此,以其中一个链表作为头节点,将另一个链表中的元素按照题目要求的递增顺序插入其中。
- 头节点设置:为了让list1和list2链表的第一个节点与其余节点的判断相同,这里在list1链表之前插入头节点dummy节点,其值未-101(题目已知链表中所有节点的范围在[-100,100]之间,选择-101可以保证若list2的头节点值大于list2头节点值时,将list2头节点的值插入在list1之前)
- 链表节点大小比较:对链表list1和list2设置指针index1Pre,index1,index2如下图所示。
当index1节点的值小于等于index2节点的值时,index1=index1.next ,index1Pre=index1Pre.next;当index1节点的值大于index2节点的值时,将index2节点插入到index1节点之前。 - 循环结束条件:当index1或index2其中一个节点为None时,结束循环
- 最后,若index2不为None,则将其拼接在index1后。
- 时间复杂度:若链表list1长度为m,链表list2长度为n,则时间复杂度为O(m+n)
3.3 代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
#list1头节点
dummy=ListNode(-101,list1)
#list1中记录前后两个指针
index1Pre,index1,index2=dummy,list1,list2
while index1!=None and index2!=None:
if index1.val<=index2.val:
index1,index1Pre=index1.next,index1Pre.next
else:
temp=ListNode(index2.val,None)
index1Pre.next=temp
temp.next=index1
index1Pre=index1Pre.next
index2=index2.next
if index2!=None:
index1Pre.next=index2
return dummy.next
4.爬楼梯(动态规划)
4.1 题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
4.2 核心思路
采用动态规划的思想,通过数组dp[i]存储爬i个楼梯的方法数目
- 初始化:当爬0个楼梯和1个楼梯时,只有一种方法,因此dp[0]=dp[1]=1
- 递归公式:dp[i]=dp[i-1]+dp[i-2]。当爬i个楼梯时,若爬i-1个楼梯用了dp[i-1]种方法,那么此时只需要爬再爬一个楼梯,就可以实现爬i个楼梯;若爬i-2个楼梯用了dp[i-2]种方法,那么此时同时爬2个楼梯,就可以实现爬i个楼梯。因此,通过加法原理,爬i个楼梯所需要的方法数目=爬i-1个楼梯所需要的方法数目+爬i-2个楼梯所需要的方法数目
- 停止条件:当爬楼梯的数目达到n时,停止计算
- 时间复杂度:O(n)
4.3 代码
class Solution:
def climbStairs(self, n: int) -> int:
dp=[0 for i in range(n+1)]
dp[0],dp[1]=1,1
for i in range(2,n+1):
dp[i]=dp[i-1]+dp[i-2]
return dp[n]
为了降低空间复杂度,可以通过两个局部变量存储dp[i-1]和dp[i-2]
5.二叉树的中序遍历(二叉树)
5.1 题目描述
5.2 核心思路
二叉树的中序遍历的顺序为:首先遍历二叉树左孩子结点,然后遍历二叉树根节点,最后遍历二叉树右孩子节点。题目通过递归或者栈来实现。
【方法一:递归】
递归主要考虑:递归参数,停止条件,递归拆解,返回值
- 递归参数:对于二叉树的中序遍历,参数为根节点root,存储中序遍历结果列表result
- 停止条件:当root为None时,无法查找其左孩子和右孩子,停止递归。
- 递归拆解:中序遍历节点顺序为左,中,右,因此对于root根节点,首先递归调用函数遍历root.left,然后将root.val的值加入result中,最后递归调用函数遍历root.right
- 返回值:result以参数形式传递给函数,因此无需返回值。
缺点:时间复杂度高,需要递归栈。
【方法二:栈】
由于二叉树的中序遍历顺序为左,中,右,因此本题核心思路是,对于root节点来说,需要先将左子树的所有左子树入栈,直至左子树的节点为None。然后将栈顶元素弹出,对于栈顶元素,将其值加入result列表中,接着遍历其右子树节点,即将右子树节点作为root节点,进行盒上述完全相同的操作。具体流程如下:
- 首先,初始result,stack列表,result作为返回值输出二叉树的中序遍历顺序,stack作为栈存储压栈的节点。
- 然后,开始不断循环。当root!=None时,循环执行root压栈操作,root=root.left;离开循环后,进行弹栈操作,root为栈顶元素,对于此时的root,将其值加入result列表,然后root=root.right,进入右子树继续遍历右子树。循环停止的条件为root==None并且stack为空。
5.3 代码
【方法一:递归】:(时间复杂度高)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
#左,中,右顺序遍历
result=[]
self.inorder(root,result)
return result
def inorder(self,root,result):
if not root:#根节点为none
return
self.inorder(root.left,result)
result.append(root.val)
self.inorder(root.right,result)
return result
【方法二:栈】
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack,result=[],[]
while root or stack:
while root!=None:
stack.append(root)
root=root.left
root=stack.pop()
result.append(root.val)
root=root.right
return result
6. 对称二叉树(二叉树)
6.1 题目描述
给你一个二叉树的根节点 root , 检查它是否轴对称。
示例:
输入:root = [1,2,2,null,3,null,3]
输出:false
输入:root = [1,2,2,3,4,4,3]
输出:true
6.2 核心思路
【方法一:递归】
对于此题,分析对称二叉树的特性如下:
- 根节点的左孩子节点的值等于右孩子节点的值
- 左孩子节点的左孩子节点的值等于右孩子节点的右孩子节点的值
- 左孩子节点的右孩子节点的值等于右孩子节点的左孩子节点的值
由此,如果我们设置函数f为递归函数,判断左右子树是否对称,则我们可以得到递归函数的要素如下:
- 递归参数:left,right;left表示左子树,right表示右子树
- 终止条件:终止条件主要包括两部分:左右子树对称情况盒左右子树不对称情况。当left和right均为空或left和right的值相同,左右子树对称,返回True;当left和right其中之一不为空,左右子树不对称,返回False
- 递归拆解:按照对称二叉树的性质,对左孩子节点的左孩子节点和右孩子节点的右孩子节点进行对称判断,对左孩子节点的右孩子节点和右孩子节点的左孩子节点进行对称判断,若其中之一不满足对称,则返回False;反之返回True
【方法二:迭代(BFS)】
迭代方法判断二叉树是否对称,思考了两种方法:1、采用层序遍历+每层节点对称判断。2、采用队列将每两个节点分为一组进行判断
层序遍历+每层节点对称判断方法:
1、初始化队列queue,将root节点入队
2、循环遍历:设置level存储每一层二叉树的节点元素。计算队列长度len(queue),分别将当前队列的所有元素弹出,针对某一个弹出的元素,将其左右节点的值插入level中,同时加入队尾。【PS:当左右节点为None时,level中插入-101(题目给出节点的值范围在[-100,100])】。最后,判断level列表是否对称,若不对称,则返回False;反之进行下一轮循环,知道queue中没有任何节点存在。
队列分组方法:
1、初始化队列queue,将root节点的左右孩子节点入队
2、循环遍历:弹出队中前两个元素leftItem,rightItem。若两个节点均为空,则继续循环;若两个节点其中之一为空,则返回False;若两个节点的值不相等,则返回False;否则,按照左子树的左孩子节点,右子树的右孩子结点,左子树的右孩子节点,右子树的左孩子节点顺序,依次入队。循环终止的条件为队列为空。
6.3 代码
【方法一:递归】
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
def compare(left,right):
if not left and not right:
return True
if (not left and right) or (not right and left):
return False
if left.val!=right.val:
return False
return compare(left.left,right.right) and compare(left.right,right.left)
return compare(root.left,root.right)
【方法二:BFS:层序遍历】
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
#层序遍历,一排一排验证:BFS
queue=deque()
queue.append(root)
while queue:
level=[]
length=len(queue)
for i in range(length):
head=queue.popleft()
if head.left!=None:
level.append(head.left.val)
queue.append(head.left)
else:
level.append(-101)
if head.right!=None:
level.append(head.right.val)
queue.append(head.right)
else:
level.append(-101)
#验证
for i in range(int(len(level)/2)):
if level[i]!=level[len(level)-i-1]:
return False
return True
【方法二:BFS-非层序遍历】
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
queue=deque()
queue.append(root.left)
queue.append(root.right)
while queue:
leftItem=queue.popleft()
rightItem=queue.popleft()
#两个节点均为空
if not leftItem and not rightItem:
continue
#两个节点其中之一为空
if not (leftItem and rightItem):
return False
#两个节点值不相同
if leftItem.val!=rightItem.val:
return False
queue.append(leftItem.left)
queue.append(rightItem.right)
queue.append(leftItem.right)
queue.append(rightItem.left)
return True
7.二叉树的最大深度(二叉树)
7.1 题目描述
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
Input: root = [3,9,20,null,null,15,7]
Output: 3
Input: root = [1,null,2]
Output: 2
7.2 核心思路
【方法一:递归】
采用递归的方法实现,其核心要素如下:
- 终止条件:当root为None时,返回树的高度为0
- 递归拆解:二叉树的最大深度是左子树或右子树的最大深度加1
【方法二:层序遍历】
设置maxDepth=1,按照6.2 的核心思路方法二,实现层序遍历,在遍历每一层后,将maxDepth+1。
7.3 代码
【方法一:递归dfs】
class Solution:
#dfs
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
return max(self.maxDepth(root.left)+1,self.maxDepth(root.right)+1)
【方法二:层序遍历BFS(队列)】
class Solution:
#BFS
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:return 0
maxDepth,queue=1,deque()
queue.append(root)
while len(queue):
length=len(queue)
for i in range(length):
head=queue.popleft()
if head.left!=None:
queue.append(head.left)
if head.right!=None:
queue.append(head.right)
if len(queue)==0:
break
maxDepth+=1
return maxDepth
8.买卖股票的最佳时机(动态规划)
8.1 题目描述
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
8.2 核心思路
采用动态规划的思想,若设置dp[i]表示第i+1天卖出股票可获得最大利润,得到动态规划要素如下:
- 初始化:第一天卖出股票时,dp[0]=0;第二天卖出股票时,最大利润只能为第一天买股票,第二天买股票。因此,dp[1]=prices[1]-prices[0]
- 递归表达式:当第i天卖股票时,存在两种可能性,在第i-1天之前买入股票和在第i-1天买入股票。因此计算两种情况下的最大利润,并求其最大值:dp[i]=max(dp[i-1]+prices[i]-prices[i-1],prices[i]-prices[i-1])
- 停止迭代:当天数到达len(prices)时,停止迭代,返回最大利润max(dp)
8.3 代码
为了减小空间复杂度,将dp列表进行压缩,用两个局部变量代替dp[i],dp[i-1]
class Solution(object):
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
if len(prices)==1:
return 0
profit,lastPro,curPro=0,0,prices[1]-prices[0]
profit=max(profit,curPro)
for i in range(2,len(prices)):
lastPro,curPro=curPro,max(prices[i]-prices[i-1],curPro+prices[i]-prices[i-1])
profit=max(profit,curPro)
return profit
9.只出现一次的数字(异或运算)
9.1 题目描述
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例:
输入:nums = [2,2,1]
输出:1
9.2 核心思路
由于时间复杂度为O(n),空间复杂度为O(1),因此无法采用哈希表(哈希表空间复杂度为O(n)),参考题解,采用异或运算。
异或运算的基本规则:0异或x=x;x异或x=0
因此,两个相同的数相异或得到0,再用0和只出现一次的数异或得到只出现一次的数。
9.3 代码
class Solution:
def singleNumber(self, nums: List[int]) -> int:
#异或运算
ans=0
for i in range(len(nums)):
ans=ans^nums[i]
return ans
10.环形链表(链表)
10.1 题目描述
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
10.2 核心思路
采用快慢指针的方式,设置slow和fast指针:
- 初始化:slow=fast=head
- 循环:当slow,fast和fast.next均不为None时,令slow=slow.next,fast=fast.next.next,若slow=fast,则存在环形链表;若slow和fast中存在None,则不存在环形链表。
10.3 代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
if not head:return False
slow,fast=head,head
while slow and fast and fast.next:
slow,fast=slow.next,fast.next.next
if slow==fast:
return True
return False
11.相交链表(链表)
11.1 题目描述
自定义评测:
评测系统 的输入如下(你设计的程序 不适用 此输入):
intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
listA - 第一个链表
listB - 第二个链表
skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数
评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。
示例:
11.2 核心思路
首先,按照下图设置链表A的特有区域长度为a,链表B特有区域长度为b,公共部分长度为c。
设置headA,headB表示链表A,B的头指针,indexA,indexB遍历两个链表。
若存在相交节点,则可以进行如下操作:
- 对于indexA来说,当indexA.next = None时,indexA到达链表A尾部,此时转向headB;
- 对于indexB来说,当indexB.next = None时,indexB到达链表B尾部,此时转向headA;
- 当indexA == indexB时,可以认为两个链表相交的节点为indexA(indexB)
若不存在相交节点,同样进行上述操作,在indexA==indexB时,indexA和indexB均为None,因此返回None
【数学推导】
1、当链表A和B存在相交节点时:
对于indexA来说,从开始遍历到遇到相交节点时,indexA走过的距离为:(a+c)+b
对于indexB来说,从开始遍历到遇到相交节点时,indexB走过的距离为:(b+c)+a
十分显然:(a+c)+b=(b+c)+a
2、当链表A和B不存在相交节点时:
对于indexA来说,从开始遍历到结束,走过的距离为a+b(此时c=0,为a+b的原因是,在循环中,若indexA.next=None时,indexA需要转向headB)
对于indexB来说,从开始遍历到结束,走过的距离为b+a
十分显然,a+b=b+a
11.3 代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
indexA,indexB=headA,headB
while indexA!=indexB:
indexA=indexA.next if indexA else headB
indexB=indexB.next if indexB else headA
return indexA
12.多数元素(哈希,分治,计数)
12.1 题目描述
给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例:
输入:nums = [3,2,3]
输出:3
12.2 核心思路
通过三种方法对其进行求解:哈希表,分治,摩尔投票
【方法一:哈希表】
显然,题目可以通过哈希表存储每一个元素出现的次数,然后遍历哈希表,找到出现次数大于 ⌊ n/2 ⌋ 的元素的值。
- 时间复杂度:构造哈希表需要遍历nums,因此,时间复杂度为O(n)
- 空间复杂度:哈希表最坏情况是nums中元素全部不相同,此时哈希表所占用的空间为O(n)
【方法二:分治】
对于计算nums数组中出现次数大于 ⌊ n/2 ⌋ 的元素的值,采用分治法的步骤如下:
- 分:将nums分为左右两部分left=nums[:len(nums)/2],right=nums[len(nums)/2:]
- 治:分别计算左右两部分出现次数大于 ⌊ n/2 ⌋ 的元素的值
- 合:若左右两部分出现次数大于 ⌊ n/2 ⌋的元素的值相同,那么合并后的nums中出现次数大于 ⌊ n/2 ⌋的元素的值就合左右两部分的值相同,直接返回即可;若左右两部分出现次数大于 ⌊ n/2 ⌋的元素的值不同,那么合并后,从新遍历nums数组,从中找到出现次数大于 ⌊ n/2 ⌋的元素的值【该值的选择,从左右两部分计算的众数中获取】,通过记录左部分数组的众数出现次数,右部分数组的众数出现次数,然后比较大小可以获取最终的众数。
时间复杂度:由分治的递归式T(n)=2T(n/2)+n以及主定理可知,时间复杂度为:O(nlogn)
空间复杂度:由于没有采用其余数据结构存储中间变量,因此空间复杂度为O(1)
【方法三:摩尔投票】
摩尔投票实现核心思路主要为:将nums数组中众数记为1,非众数记为-1,然后将其相加,可以得到一个大于0的数。【若某一个数不是众数,但将其设置为1,其余数设置为-1,那么所有元素相加后得到的数位负数】
摩尔投票实现过程如下:
- 初始化candidate,count为0,其中candidate记录当前候选众数,count用于计数(计数就是核心思路中的1和-1相加的最终结果)。
- 遍历nums中的元素,若count=0,则candidate=nums[i];在count!=0时,若nums[i]=candidate,则count++;若nums[i]!=candidate,则count–。
- 最终返回candidate。
【算法的正确性证明】
假设maj为nums数组中真正的众数,value作为计数。 当nums[i]=maj时,value++;当nums[i]!=maj时,value–。通过下方样例可以看出:
count和value互为相反数或者相等,最终value和count的值均为正数(为什么为正数?因为众数出现的次数必然比非众数多,此时1代表众数,-1代表非众数,相加后必大于0)。
因此,可以得到最终的candidate的值就是最终的众数。
12.3 代码
【方法一:哈希表】
class Solution:
#哈希表:时间复杂度O(n),空间复杂度O(n)
def majorityElement(self, nums: List[int]) -> int:
counter,n=Counter(nums),len(nums)
for key,value in counter.items():
if value>int(n/2):
return key
【方法二:分治】
class Solution:
#分治
def majorityElement(self, nums: List[int]) -> int:
if len(nums)==1:
return nums[0]
#将nums分为左右两部分
n=len(nums)
left,right=nums[:int(n/2)],nums[int(n/2):]
#分别计算左右两部分众数
leftNum=self.majorityElement(left)
rightNum=self.majorityElement(right)
#若左边众数和右边众数相同,则合并后的数组众数与两者相同
if leftNum==rightNum:
return rightNum
else:#否则,返回合并后的众数,合并后的众数从左右两部分的众数之中选择
leftCount,rightCount=0,0
for i in range(n):
if nums[i]==leftNum:
leftCount+=1
if nums[i]==rightNum:
rightCount+=1
return rightNum if rightCount>leftCount else leftNum
【方法三:摩尔投票】
class Solution:
def majorityElement(self, nums: List[int]) -> int:
candidate,count=0,0
for i in range(len(nums)):
if count==0:
candidate=nums[i]
if candidate==nums[i]:
count+=1
elif candidate!=nums[i]:
count-=1
return candidate
13.反转链表(链表)
13.1 题目描述
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表
13.2 核心思路
通过迭代的方法,对链表进行逆序。
首先,船舰头节点dummy,指向head。然后循环遍历链表,每一次遍历交换head与head.next的节点的值。交换过程如下所示:
最终,当head.next不存在时,结束循环。
13.3 代码
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:return None
dummy=ListNode(0,head)
while head.next!=None:
temp=dummy.next
dummy.next=head.next
head.next=dummy.next.next
dummy.next.next=temp
return dummy.next
14.翻转二叉树(二叉树)
14.1 题目描述
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
14.2 核心思路
对于二叉树的题目,存在两种方法:DFS和BFS
【方法一:DFS(递归)】
对于一个以root为根节点的二叉树来说,我们要对其进行翻转操作,就需要其左右子树是已经翻转的,然后在交换其做右孩子结点即可。因此,递归方法思路如下:
- 若root为空,则停止递归
- 若root不为空,则分别递归调用函数,对左子树和右子树进行翻转,然后root.left和root.right进行交换
- 最后,返回root
【方法二:BFS-队列】
-
构造队列queue,对于root根节点,若root为空,则直接返回root;若root不为空,则将其入队。
-
在队列不为空时,开始循环弹出队列队首元素。head=queue.pop()。然后交换head
的左右节点,若head的左右节点不为空,则将其入队。 -
最后,返回root。
14.3 代码
【方法一:递归】
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
def exchange(root):
if not root:
return
root.left,root.right=root.right,root.left
exchange(root.left)
exchange(root.right)
exchange(root)
return root
【方法二:队列BFS】
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:return root
queue=deque()
queue.append(root)
while len(queue)!=0:
head=queue.popleft()
head.left,head.right=head.right,head.left
if head.left:
queue.append(head.left)
if head.right:
queue.append(head.right)
return root
15.回文链表(链表)
15.1 题目描述
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
示例:
输入:head = [1,2,2,1]
输出:true
15.2 核心思路
【方法一:链表转化为数组】
可以遍历链表,并将其中的元素存储在数组中,然后使用首尾双指针判断数组是否为回文数组。
时间复杂度:O(n)
空间复杂度:O(n)
【方法二】
参考了题解,为了降低空间复杂度,改变链表结构,将链表后半部分及进行逆序,然后和链表前半部分的数据进行比较,判断是否为回文。主要分为3步:
- 步骤一:使用快慢指针寻找到链表中间位置。初始化慢指针slow和快指针fast为链表头指针head,slow每次移动一位,fast每次移动两位,最终在fast为空时,slow指针指向链表的中间位置,将链表分为了两个部分。在接下来的步骤中,会对这两个部分是否为回文进行判断。
- 步骤二:以slow为头节点,将slow指向的后半部分指针进行逆序排序。【参考13.反转链表(链表)】
- 步骤三:将反转后的链表和以head节点为首的链表前半部分按照次序进行比较,若出现不相等的值,则返回False;反之最终返回True
15.3 代码
【方法二】
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
if not head.next:return True
#步骤一:使用快慢指针寻找到链表中间位置
slow,fast=head,head
while fast:
slow=slow.next
fast=fast.next.next if fast.next and fast.next.next else None
#步骤二:对于链表后半部分进行反转
def reverseLink(head):
dummy=ListNode(0,head)
while head.next:
temp=dummy.next
dummy.next=head.next
head.next=dummy.next.next
dummy.next.next=temp
return dummy.next
slow=reverseLink(slow)
#步骤三:判断是否为回文链表
while slow:
if slow.val!=head.val:
return False
slow,head=slow.next,head.next
return True
16.移动零(数组)
16.1 题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
16.2 核心思路
采用类似快排的划分思想,以非0数位基准,左边位非零数,右边为0
通过zeroindex记录第一个为0的元素的下标。然后循环遍历nums数组,当nums[i]!=0时,nums[i]和nums[zeroindex]进行交换,然后zeroindex++
时间复杂度:O(n)
空间复杂度:O(1)
16.3 代码
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
zeroindex=-1
for i in range(len(nums)):
if nums[i]==0 and zeroindex==-1:
zeroindex=i
elif nums[i]!=0 and zeroindex!=-1:
nums[zeroindex],nums[i]=nums[i],nums[zeroindex]
zeroindex+=1
return nums
17.比特位计数(动态规划,位运算)
17.1 题目描述
给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
示例:
输入:n = 2
输出:[0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10
17.2 核心思路
采用动态规划的思想实现。用dp[i]存储十进制i的二进制1出现的次数。
- 对于某一个数x来说,如果知道了在[0,x)之间的某一个数y,y的二进制除了最高位为0,其余的的位数与x的二进制相同,那么,可以认为dp[x]=dp[y]+1
【举例说明:2的二进制为010,6的二进制为110,两者除了最高位不同,其余位数相同】
- 通过观察可以发现,对于2次幂的二进制来说,只有最高位为1,其余位数均为0。若已知k=2^i,那么位于(k+1,2k))之间的数,可以通过(k/2,k)得到。
【举例说明:6是在4和8之间的数,为计算6的二进制1出现的次数,可以首先获得2的二进制1出现的次数,然后加1。dp[6]=dp[2]+1】
- 如何知道递推式dp[x]=dp[y]+1中y是多少。可以通过观察得到,x-y的差值为2次幂,这个2次幂为小于x的最大2次幂。因此,在动态规划时,遇到2次幂时需要更新当前最大的2次幂。
【举例说明:dp[6]=dp[2]+1,6-2=4,4为小于6的最大二次幂】
17.3 代码
class Solution:
def countBits(self, n: int) -> List[int]:
if n==0:return [0]
dp=[0 for i in range(n+1)]
preBit=0
for i in range(1,n+1):
if i&(i-1)==0:#为2次幂
preBit=i
dp[i]=dp[i-preBit]+1
return dp
【tips:判断n是否为2次幂的方法:n&(n-1)==0。若n为2次幂,则其最高位为1,其余位为0;n-1最高位为0,其余位为1,两者&运算得到0】
18.找到所有数组中消失的数字(哈希表)
18.1 题目描述
给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
示例:
输入:nums = [4,3,2,7,8,2,3,1]
输出:[5,6]
18.2 核心思路
创建一个哈希表,记录nums数组中每一个数出现的次数;遍历[1,n]之间的数,并在哈希表中查询是否出现,若不出现,则将其加入result列表中返回。
【优化:减小空间复杂度】
由于哈希表的大小最大为n,而nums数组的长度也为n,因此直接将nums数组原地修改作为哈希表。
遍历nums数组,对于nums[i],将以nums[i]%n-1为下标的nums数组中的元素增加n。这样遍历nums数组,最终可以得到原nums数组中出现过的元素x,nums[x-1]的值必然大于n,而未出现的元素y,nums[y]<=n。
最终,再次遍历nums数组,找到满足条件的nums[i]<=n,并将i+1,加入result列表返回。
18.3 代码
【未优化】
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
n=len(nums)
counter=Counter(nums)
result=[]
for i in range(1,n+1):
if i not in counter:
result.append(i)
return result
【优化】
class Solution:
def findDisappearedNumbers(self, nums: List[int]) -> List[int]:
n,result=len(nums),[]
for i in range(len(nums)):
nums[nums[i]%n-1]+=n
for i in range(n):
if nums[i]<=n:
result.append(i+1)
return result
19.汉明距离(位运算)
19.1 题目描述
两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。
给你两个整数 x 和 y,计算并返回它们之间的汉明距离。
示例:
输入:x = 1, y = 4
输出:2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
19.2 核心思路
通过位运算实现,已知x和y两个数,为了计算出两者之间二进制不同的位数有几位,采用异或运算。x异或y中1出现的次数就是两者的汉明距离。
参考了官方题解,主要有两种方式计算1出现的次数
【方法一】
对于数num来说,我们将其和1进行与运算(&),可以看到当num的最低位为1时,num&1=1;当num最低位为0时,num&1=0。
因此不断进行num&1运算,然后对num向右移动一位(num>>1),直到num=0为止,同时记录最后一位为1的次数,作为最终结果。
【方法二:brian kernighan算法】
19.3 代码
【方法一】
class Solution:
def hammingDistance(self, x: int, y: int) -> int:
result,countNum=x^y,0
while result:
countNum+=result&1
result=result>>1
return countNum
【方法二】
class Solution:
def hammingDistance(self, x: int, y: int) -> int:
result,countNum=x^y,0
while result:
result=result&(result-1)
countNum+=1
return countNum
20.二叉树的直径(二叉树)
20.1 题目描述
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
20.2 核心思路
采用递归的方法进行求解。
首先,对于一个以root为根节点的二叉树来说,如果其最大直径一定要通过root,那么最大的直径为:左子树深度+右子树深度
因此,获取以root为根节点的最大直径,然后递归遍历获取root.left和root.right为根节点的最大直径,不断更新整体的最大值maxDiameter即可。
20.3 代码
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
maxDiameter=0
#获取以root为根节点,左右子树的深度
def getDepth(root):
nonlocal maxDiameter
if not root:return 0
LDepth=getDepth(root.left)
RDepth=getDepth(root.right)
#更新maxDiameter
maxDiameter=max(maxDiameter,LDepth+RDepth)
return max(LDepth,RDepth)+1
getDepth(root)
return maxDiameter
21.合并二叉树(二叉树)
21.1 题目描述
给你两棵二叉树: root1 和 root2 。
想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会)。你需要将这两棵树合并成一棵新二叉树。合并的规则是:如果两个节点重叠,那么将这两个节点的值相加作为合并后节点的新值;否则,不为 null 的节点将直接作为新二叉树的节点。
返回合并后的二叉树。
注意: 合并过程必须从两个树的根节点开始。
21.2 核心思路
采用递归的思想,用dfs对二叉树进行遍历并进行合并。root1和root2表示两个二叉树的根节点。
- 若root1和root2其中之一为空,则返回另一个二叉树
- 若两者均不为空,则将其节点的值相加,同时产生一个新的根节点mergeTree,mergeTree=TreeNode(root1.val+root2.val),并递归调用函数,将root1和roo2的左子树合并成为mergeTree的左子树;将root1和roo2的右子树合并成为mergeTree的右子树。
21.3 代码
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def mergeTrees(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> Optional[TreeNode]:
if not root1:
return root2
if not root2:
return root1
mergeTree=TreeNode(root1.val+root2.val)
mergeTree.left=self.mergeTrees(root1.left,root2.left)
mergeTree.right=self.mergeTrees(root1.right,root2.right)
return mergeTree