88. 合并两个有序数组
解题思路:
双指针加单指针
同时从后往前遍历原始的nums1和2,比较大小,大的往后站。
class Solution:def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:"""Do not return anything, modify nums1 in-place instead."""# 初始化双指针,还有全局指针index1, index2, index = m-1, n-1, m+n-1# 从后往前遍历while index1 >= 0 and index2 >= 0:# 如果nums1的元素大,就放在最后if nums1[index1] > nums2[index2]:nums1[index] = nums1[index1]index1 -= 1else:nums1[index] = nums2[index2]index2 -= 1index -= 1# nums2有剩余元素if index2 >= 0:nums1[:index2+1] = nums2[:index2+1]return(nums1)
当面对 “合并两个有序数组” 的任务时,特别是在给定 nums1
中有足够的空间来容纳 nums2
中的所有元素这一约束条件下,我们可以通过一个有效的原地(in-place)合并方法来实现。这里的 “原地” 指的是不使用额外的空间来存储大量的数据,仅利用输入数组的空间。我们从后向前填充 nums1
,这样做可以避免在合并时覆盖 nums1
中尚未处理的元素。下面是详细的步骤说明:
算法步骤
-
初始化指针:
index1
指向nums1
中最后一个有效元素的位置,即m-1
。index2
指向nums2
中最后一个元素的位置,即n-1
。index
指向nums1
的最末尾位置,即m + n - 1
。这是合并后的数组的最后一个位置。
-
从后向前比较并合并:
- 当
index1 >= 0
和index2 >= 0
时,比较nums1[index1]
和nums2[index2]
:- 如果
nums1[index1]
大于等于nums2[index2]
,则将nums1[index1]
放在nums1[index]
的位置上,然后index1
减一。 - 如果
nums1[index1]
小于nums2[index2]
,则将nums2[index2]
放在nums1[index]
的位置上,然后index2
减一。
- 如果
- 每次操作后,
index
减一,以准备下一个位置的填充。
- 当
-
处理剩余元素:
- 如果
nums2
中还有未合并的元素(当index2 >= 0
时),直接将这些剩余元素复制到nums1
的前面部分,即从nums1[0]
到nums1[index2]
(因为index2
未被合并的部分恰好表示还需要合并多少元素)。
- 如果
-
完成合并:
- 在上述步骤完成后,
nums1
就包含了完全合并后的有序数组。
- 在上述步骤完成后,
示例
假设 nums1 = [1,2,3,0,0,0]
, m = 3
, nums2 = [2,5,6]
, n = 3
。
-
初始状态:
nums1 = [1, 2, 3, 0, 0, 0]
nums2 = [2, 5, 6]
-
处理过程:
- 比较
3
和6
,将6
放在最后,更新数组为[1, 2, 3, 0, 0, 6]
- 比较
3
和5
,将5
放在倒数第二的位置,更新数组为[1, 2, 3, 0, 5, 6]
- 比较
3
和2
,将3
放在倒数第三的位置,更新数组为[1, 2, 3, 3, 5, 6]
- 比较
2
和2
,将2
放在倒数第四的位置,更新数组为[1, 2, 2, 3, 5, 6]
- 处理完成,没有剩余元素需要特殊处理。
- 比较
169. 多数元素
解题思路1:哈希表计数,再找到最大的。
class Solution:def majorityElement(self, nums: List[int]) -> int:# 添加哈希表元素_dict = {}for i in nums:if i not in _dict:_dict[i] = 1else:_dict[i] += 1# 遍历哈希表,找到最大的max_num = 0max_can = 0for key,value in _dict.items():if value > max_num:max_num = valuemax_can = keyreturn max_can
解题思路2:
摩尔投票算法
class Solution:def majorityElement(self, nums: List[int]) -> int:# 将多数元素与非多数元素一起成对消除,最后留下的就是多数# 初始化计数和候选count = 0candicate = None# 遍历for num in nums:# 如果此前多数元素消除完了,就拿当前的作为多数if count == 0:candicate = num# 计数更新count += (1 if candicate == num else -1)return candicate
这段代码实现了一个寻找数组中多数元素的算法,使用的是摩尔投票算法(Boyer-Moore Voting Algorithm)。这个算法特别适用于找出一个数组中出现次数超过半数的元素。下面是算法的步骤解释:
算法步骤
-
初始化:
- 设置一个计数器
count
,初始值为0。 - 设置一个变量
max_n
用于存储可能的多数元素,初始值为None
。
- 设置一个计数器
-
遍历数组:
- 对数组
nums
中的每个元素num
进行迭代。
- 对数组
-
确定候选元素:
- 当计数器
count
为0时,将当前遍历到的元素num
设为新的候选多数元素,并将max_n
设置为这个元素。 - 此步骤确保如果之前的候选元素不是真正的多数元素,可以被更有可能的候选者替换。
- 当计数器
-
更新计数器:
- 如果当前元素
num
等于候选多数元素max_n
,计数器count
加1。 - 否则,计数器
count
减1。 - 这一步的逻辑是:每遇到一个与
max_n
相同的元素,就增加它的权重;每遇到一个不同的元素,就减少它的权重。这样做的目的是抵消那些非多数元素的影响。
- 如果当前元素
-
返回结果:
- 遍历完成后,
max_n
中存储的就是数组的多数元素。这是基于问题描述中的假设,即一定存在一个多数元素。
- 遍历完成后,
代码功能
这个算法非常高效,只需要一次遍历(时间复杂度为 (O(n))),并且只使用了常数空间(空间复杂度为 (O(1)))。算法的核心在于通过计数器的增减来维护当前可能的多数元素,其基本假设是多数元素的数量比其他所有元素的数量总和还要多,因此最终 max_n
中存储的元素必然是真正的多数元素。
应用场景
这种算法适用于需要从一个大数据集中快速确定主要趋势或者占优势数量的元素的场景,如社交媒体数据分析、市场调研等领域中的统计分析。
136. 只出现一次的数字
要求常量空间内解决。所以思路1是不行的。
思路1:
新建一个list,遍历nums,有相同的就去掉原来的相同的,这样留下的就是所求。
也可以使用哈希表辅助。
class Solution:def singleNumber(self, nums: List[int]) -> int:# 假设第一个是res = []for i in nums:if i not in res:res.append(i)else:res.remove(i)return sum(res)
正确解法:
位运算。
class Solution:def singleNumber(self, nums: List[int]) -> int:# 结果假设res = 0# 遍历,形成连乘的异或运算for num in nums:res ^= numreturn res
位运算由于其独特的性质,在解决编程问题,特别是在竞赛和面试中经常被用来简化解决方案或提高运算效率。以下是一些重要的位运算性质,特别强调了异或运算(XOR),这些性质可以帮助你更好地理解和运用位运算解决实际问题:
通用性质
零的规律:
- 任何数与0进行按位与、按位或和按位异或操作的结果:
a & 0 = 0
a | 0 = a
a ^ 0 = a
自身规律:
- 任何数与自身进行按位与、按位或和按位异或操作的结果:
a & a = a
a | a = a
a ^ a = 0
(异或的重要性质)按位取反:
~a
等于-a - 1
(在二进制表示中,所有位数取反)异或运算(XOR)的特殊性质 异或运算是位运算中极具特色的一种,它在算法问题中尤为常见,具有以下几个重要性质:
反身性:
- 任何数与自己异或的结果为0,即
a ^ a = 0
。恒等性:
- 任何数与0异或仍然为自己,即
a ^ 0 = a
。交换律和结合律:
a ^ b = b ^ a
a ^ (b ^ c) = (a ^ b) ^ c
- 这意味着多个数异或的结果与顺序无关。
自反性:
- 如果
a ^ b = c
,那么a ^ c = b
也成立,且b ^ c = a
。这意味着异或操作可以用来在不使用第三个变量的情况下交换两个数。应用场景举例
找出唯一的单一数字:
- 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。可以通过对所有元素进行异或操作解决,因为成对的数字会通过
a ^ a = 0
消除,剩下的就是唯一的单一数字。交换两个变量:
- 不使用临时变量交换两个数:
a ^= b; b ^= a; a ^= b;
这样就完成了a
和b
的交换。构建简单的加密和解密:
- 由于
a ^ b = c
可逆,c ^ b = a
,可以使用异或来对数据进行简单的加密和解密。
56. 合并区间
解题思路:
合并区间排序,一定要以元素的左边界排序!
比如这样的情况 处理 [2, 3] 和 [1, 4],应该合并为[1,4],但是按右边界的话,就是[2,3],[1,4],就变成了[2,4],这是错误的。
步骤:
1.按照元素的左边界升序排列。lambda表达式
2.初始化结果列表,并将第一个元素添加进,作为基准元素
3.遍历剩余的元素,如果结果列表的最后一个元素(已经是当前最大了)的右边界小于当前遍历到的元素的左边界,说明重叠,此时的新元素的左边界是结果列表的最后一个元素的左边界,右边界由这两个元素的右边界决定,选择最大值。
class Solution:def merge(self, intervals: List[List[int]]) -> List[List[int]]:# 按照左边界升序排序intervals.sort(key=lambda x: x[0])# 初始化res列表res = [intervals[0]]for i in intervals[1:]:# 前一个元素的右边界小于当前元素的左边界,直接添加当前元素if res[-1][1] < i[0]:res.append(i)# 重叠,因为已经按左边界升序排列了,所以重叠时的合并区间的左边界就是res[-1]的# 至于右边界,就看是前一个元素还是当前遍历到的元素的右边界大了# 比如 [1,2],[2,3]else:res[-1][1] = max(res[-1][1], i[1])return res
179. 最大数
解题思路:
利用字典序,对数组排序。
步骤:
1.将数组元素转为str
2.设置自定义排序函数,不断比较两个元素不同位置拼接的结果大小,选择最大的拼接方式
3.排序,检查是否有前导0
4.输出为字符串
class Solution:def largestNumber(self, nums):# 将数字转换为字符串,便于拼接比较strs = [str(num) for num in nums]# 自定义排序函数,看这两个值的def compare(x,y):# 如果xy拼接>yx拼接,说明x应该放在y前面,因为用正序排列,返回-1if x + y > y + x:return -1elif x + y < y + x:return 1else:return 0# 排序,使用自定义函数strs.sort(key=cmp_to_key(compare))# 检查元素,如果第一个是0,说明是全0if strs[0] == '0':return '0'# 正常返回return ''.join(strs)
排序流程概述:
排序算法开始:排序算法(如快速排序)会选择一个元素作为基准,然后将其他元素与这个基准进行比较,这涉及到多次调用
compare 函数。 比较操作:每次比较都涉及两个元素。比如,可能先比较 3 和 30,基于 compare 函数的结果(比较 "330"与 “303”),确定它们在数组中的相对位置。任何两个元素都可能被比较,不仅限于紧邻的元素。
递归和迭代:排序算法递归地对基准点左侧和右侧的子数组进行相同的过程,不断比较和交换位置,直到整个数组有序。
704. 二分查找
解题思路:
左右指针,确定中点,不断更新中点和左右指针,缩小范围,找到目标数。
步骤:
1.初始化左右指针分别指向数组左右边界
2.使用while循环,条件是左指针小于等于右指针
3.每次循环都更新mid,这里要注意,因为不断更新左指针,但是数组还是那么长。所以mid不等于(right-left+1)//2,而是left+(right-left)//2
4.循环体中判断是否mid就是目标值索引。如果大于mid的值,那就说明在mid的后面,更新left=mid+1,因为判断中已经把mid包括了。小于同理
5.默认返回-1
class Solution:def search(self, nums: List[int], target: int) -> int:n = len(nums)# 左右边界指针初始化left, right = 0, n-1# 当还未遍历完数组while left <= right:# 中间位置,必须是以左界为起点,这样才能在第二轮及之后都找到正确的中点mid = left + (right-left)//2# 如果相等,就找到了,返回索引if target == nums[mid]:return mid# 如果小于中点的元素,就要更新右指针,因为mid处已经被考虑过了,所以减1elif target < nums[mid]:right = mid-1# 大于同理else:left = mid+1# 最后返回默认值return -1
34. 在排序数组中查找元素的第一个和最后一个位置
解题思路1:
时间复杂度O(n),纯粹遍历
class Solution:def searchRange(self, nums: List[int], target: int) -> List[int]:start, end = -1,-1for i in range(len(nums)):if start == -1 and nums[i] == target:start = iif start != -1 and nums[i] == target:end = ireturn [start, end]
正确的思路:
二分查找。
分阶段,第一阶段找首次出现的位置,第二阶段找第二次。
要注意的是每阶段都要检查前面或者后面是否有相同元素,以确保找到的就是所求。
class Solution:def searchRange(self, nums: List[int], target: int) -> List[int]:# 先找第一个元素def first(nums, target):left,right = 0, len(nums)-1while left <= right:mid = left+(right-left)//2# 大于中点,更新左边界if nums[mid] < target:left = mid+1elif nums[mid] > target:right = mid-1else:# 检查前面是否还有该元素,更新右边界,逐步往前if mid == 0 or nums[mid-1] != target:return midright = mid-1return -1# 再找最后一个元素def end(nums, target):left,right = 0, len(nums)-1while left <= right:mid = left+(right-left)//2if nums[mid] < target:left = mid+1elif nums[mid] > target:right = mid-1else:# 检查后面是否还有该元素,更新左边界,逐步往后if mid == len(nums)-1 or nums[mid+1] != target:return midleft = mid + 1return -1# 找第一个元素start = first(nums, target)# 没找到就返回-1if start == -1:return [-1,-1]# 找到了第一个元素,找第二个last = end(nums, target)return [start, last]
153. 寻找旋转排序数组中的最小值
解题思路:
使用二分查找,将右边界的值作为target。
多次旋转后,如果中点大于右边界,说明最小值在右边,向右缩小区间,更新left为mid+1
如果小于等于右边界,说明是mid或者在左边,更新right 为mid
继续循环。
class Solution:def findMin(self, nums: List[int]) -> int:left, right = 0, len(nums)-1# 二分查找while left < right:mid = left + (right-left)//2# 右端点的值大于中点,说明最小值是mid或者在mid的左边# 更新右边界if nums[mid] <= nums[right]:right = midelse:# 小于中点,说明在mid的右边left = mid + 1 return nums[left]
33. 搜索旋转排序数组
解题思路:
旋转之后就不能用原始的二分查找了。
步骤:
1.初始化左右界指针
2.开始二分查找的循环
3.循环体:
初始化中点,如果中点就是目标值,那么就返回中点索引。
首先判断左边是否有序,进一步判断目标值是在left——mid吗?如果是,就更新右指针;否则,更新左指针。
再判断右边是否有序,同理。
默认返回-1
class Solution:def search(self, nums: List[int], target: int) -> int:# 二分查找left, right = 0,len(nums)-1while left <= right:mid = left + (right-left)//2# 找到目标值,返回下标if nums[mid] == target:return mid# 因为是升序,所以说明左边是有序的if nums[mid] >= nums[left]:# 目标在左边,缩小右边的区间if nums[left] <= target <= nums[mid]:right = mid - 1# 否则,缩小左边的区间else:left = mid + 1# 右边是有序的,同理操作else:if nums[mid] <= target <= nums[right]:left = mid+1else:right = mid-1return -1
算法思路
这个问题可以使用二分查找的变体来解决,关键是如何在旋转的数组中应用二分查找。具体的方法是使用两个指针
left
和right
分别指向数组的起始和结束位置,并在每一步中确定旋转点是否在左边或右边,以及目标值是否可能位于当前考虑的数组部分。
- 初始化指针:
left
指向数组的第一个元素,right
指向最后一个元素。- 计算中点:在每次迭代中,计算中点
mid = (left + right) // 2
。- 比较和移动:
- 如果
nums[mid]
等于target
,返回mid
。- 确定有序区间:
- 如果
nums[left] <= nums[mid]
,这说明左半部分是有序的。
- 如果
target
在nums[left]
和nums[mid]
之间,移动right
到mid - 1
。- 否则,移动
left
到mid + 1
。- 否则,右半部分是有序的。
- 如果
target
在nums[mid]
和nums[right]
之间,移动left
到mid + 1
。- 否则,移动
right
到mid - 1
。- 循环结束:如果整个数组都搜索完毕仍没有找到
target
,返回-1
。
162. 寻找峰值
解题思路:
也是二分查找。
根据中点和左右相邻的大小关系来确定峰值在哪边,然后收敛峰值到left=right
class Solution:def findPeakElement(self, nums: List[int]) -> int:left, right = 0, len(nums)-1# 二分查找while left < right:mid = left + (right-left)//2# 峰值在右侧,中点右边相邻的比中点大if nums[mid] < nums[mid+1]:left = mid + 1# 峰值在左侧else:right = mid# 返回左指针return left
240. 搜索二维矩阵 II
解题思路:
矩阵可以理解为从西北到东南是逐步增加的。
具体来说,是每行是从左到右升序,每列是从上到下升序。
所以就根据这个性质逐步缩小范围:如果当前元素比目标值大,说明太右边了,可以排除掉这一列;比目标值小,说明太上面了,这一行可以排除。
这种方法通常被称为“步进”方法或“Z字形”搜索。
步骤:
1.初始化矩阵长宽m、n
2.设置初始坐标为右上角,使得最右边最大,最上面最小。以利用矩阵性质
3.判断是否符合条件
4.缩小范围:太大就往左移动,太小就往下移动。
class Solution:def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:# 矩阵的长宽m,n = len(matrix), len(matrix[0])# 初始化矩阵的长宽边界i,j = 0,n-1# 在矩阵范围内搜索while i < m and j >= 0:# 如果当前元素等于目标值if matrix[i][j] == target:return True# 如果太大了,说明当前元素靠右了,往左移动if matrix[i][j] > target:j -= 1# 如果太小了,说明靠上了,往下移动else:i += 1return False
69. x 的平方根
没想到这题也能用二分查找。
平方根肯定是在0-x之间的某个数,所以可以将其看做一个长度为x的数组,这样就能进行二分查找。
我们的target就是平方根,得到公式target**target=x
步骤:
1.初始化左右指针
2.二分查找,判断条件是中点是否满足该公式
3.默认返回右指针,这是因为循环退出时正好指向小于等于平方根的最大整数
class Solution:def mySqrt(self, x: int) -> int:# 以该数作为数组,进行二分查找left, right = 0, xwhile left <= right:mid = left+(right-left)//2# 如果在从0到数字的区间内有中点正好是平方根,那么相乘就是该数if mid * mid == x:return mid# 更新左右指针elif mid * mid < x:left = mid + 1else:right = mid - 1# 默认返回右指针# 因为循环退出时right指向小于等于sqrt(x)的最大整数return right
283. 移动零
解题思路:左右双指针
左指针用于构造数组前端的非0,右指针用于遍历并检查非0元素
class Solution:def moveZeroes(self, nums: List[int]) -> None:"""Do not return anything, modify nums in-place instead."""# 左右指针,左指针用于构造数组前端left = 0# 右指针遍历数组for right in range(len(nums)):# 发现一个非0元素,并且两个指针不相等,就交换两个指针的元素值if nums[right] != 0:if right != left:nums[left], nums[right] = nums[right], nums[left]# 右指针不是指向0,就可以把左指针往右移动left += 1return nums
算法步骤解析
初始化指针:
left
指针用于跟踪在数组中应该插入下一个非零元素的位置。初始化为0,指向数组的起始位置。遍历数组:
- 使用
right
指针遍历整个数组。right
指针用于检查每个元素是否为非零。right
从0开始,直到数组末尾。检查并交换:
- 如果
nums[right]
(当前元素)不为零,进入交换逻辑:
- 检查是否需要交换:如果
right
指针的位置不等于left
指针的位置(这意味着right
和left
之间至少有一个零),则执行交换操作:
- 将
nums[right]
(非零元素)和nums[left]
(left
指向的是第一个零元素的位置或者是另一个非零元素的位置)进行交换。- 移动
left
指针:无论是否进行了交换,left
指针都需要向右移动一位,因为现在left
的位置已经填充了一个非零元素,下一个可能的非零元素应该放在left + 1
的位置。循环直到数组结束:
- 继续上述步骤直到
right
指针遍历完数组。结果
- 该方法通过一次遍历完成了非零元素的前移和零的后移操作。
- 这种策略保证了只有当非零元素和零之间有间隔时才进行交换,减少了不必要的操作,增加了算法的效率。
- 在遍历结束后,所有的非零元素都保持了它们的相对位置并且排在数组的前面,所有的零都移动到了数组的后部。
性能