测试&测开面试要知道的那些事--算法篇(LeetCode_Python)

LeetCode.两数之和

题目

给定一个整数数组 nums 和一个目标值 target在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标不能重复利用这个数组中同样的元素。

示例:

给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

解题思路:

创建空字典,将目标值与 nums 列表中的数值之差存储在字典中,同时与其他数值进行比对 。

字典中的键(key)对应的是目标值与列表数值的差(即是 target - nums[i]),而字典的值(value)对应的是当前 nums[i] 的索引位置 i。

如果 nums[i] 在字典中,(注意: i 是变化的值,为了防止跟前面的混淆,前面的 “target - nums[i]” 写作 “target - nums[k]” 以作区分),也就是说 target - nums[k] = nums[i] (表明前面字典存储的键当中,有跟当前的 nums[i] 相等的数值),此时返回 target - nums[k] 在字典中对应的值,也即是 nums[k] 的索引位置 k,以及当前 nums[i] 的索引位置 i。两者就是要求的结果。

代码实现如下:

class Solution:
    def twoSum(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: List[int]
        """
        # 创建空字典
        # 存储目标值与 nums 列表其中一个数值之差,同时与其他数值进行比对
        # 相等返回对应的索引
        auxiliary_dict = {}

        # 获取 nums 列表的长度
        nums_len = len(nums)

        for i in range(nums_len):
            # 目标值与对应 nums 列表 i 索引中的数值之差
            two_sub = target - nums[i]
            # 如果 nums[i] 在 auxiliary_dict 列表中,返回对应的索引
            # 这里 nums[i] 是变化的,i 值不断变化。此时的 num[i] 如果在字典中,表示前面某次当中,目标值与其中一个数值之差等于当前的 num[i]
            if nums[i] in auxiliary_dict:
                return auxiliary_dict[nums[i]], i

            # 否则,往字典中添加键值对
            auxiliary_dict[two_sub] = i

LeetCode.回文数

题目

判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。

示例 1:

输入: 121
输出: true

示例 2:

输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。

示例 3:

输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。

解题思路:

  1. 第一种方法,虑数字转换为字符串,判断字符串是否是回文数,但中间需要创建额外的空间。
  2. 第二种方法,考虑直接将数字反转,与原数字进行比较。中间可能会产生整数溢出的情况。
  3. 在第二个方法的前提下,考虑将数字分为前后两部分,反转后面部分,与前面部分进行比较。

实现代码如下:

class Solution:
    def isPalindrome(self, x: int) -> bool:
    	'''判断数字是否是回文数
		
		Args:
		    x: 传入的待判断的数字
		
		Returns:
		    返回是否是回文数的判断结果,结果类型为布尔型
    	'''
        # 首先负数不是回文数,
        # 当传进来的数字非负,但最后一位数为 0 时,
        # 只有当数字为 0,才符合回文数的要求。
        if (x < 0 ) or (x != 0 and x % 10 == 0):
            return False
        
        # 将传入的数字分为前后两部分,
        # 将后半部分进行倒序,与前面部分进行比较,
        # 相等则为回文数,否则不是回文数。
        cur = 0  # 用于存储后半部分数字
        while (x > cur):
            cur = cur * 10 + x % 10
            x //= 10
        
        # 考虑数字位数为奇数的情况下,可以用 cur // 10 进行消除中间数
        return (x == cur) or (x == (cur // 10))

LeetCode.有效的括号

题目

给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。

有效字符串需满足:

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

注意空字符串可被认为是有效字符串。

示例1:

输入: "()"
输出: true

示例2:

输入: "()[]{}"
输出: true

示例3:

输入: "([)]"
输出: false

示例4:

输入: "{[]}"
输出: true

解题思路

  1. 思路:栈
  2. 对括号进行映射,右括号为键,左括号为值;
  3. 如果遇到左括号,直接将其入栈。等待后面匹配处理;
  4. 如果遇到右括号,检查栈顶的元素。若是同类型则弹出继续处理接下的部分。否则,直接返回 False;
  5. 如果最终遍历完成后,栈中还有元素,同样返回 False。

代码实现如下:

class Solution:
    def isValid(self, s: str) -> bool:
        '''判断是否是有效的括号

        Args:
            str: 包含括号的字符串
        
        Returns:
            返回判断的结果,满足条件:
                1. 左括号必须用相同的类型的右括号闭合。
                2. 左括号必须以正确的顺序闭合。
            空字符串可以被认为是有效的字符串
            返回类型为布尔型
        '''
        # 以栈形式存储左括号
        stack = []

        # 以右括号当成键映射对应类型的左括号
        prths_mapping = {'}': '{', ']': '[', ')': '('}

        # 遍历字符串,遇左括号则进行入栈
        for ch in s:
            # 对字符进行判断,是否为右括号
            if ch in prths_mapping:
                # 为右括号的情况下,判断 stack 栈顶是否是同类型的左括号
                # pop 出栈顶的字符
                # 若 stack 为空,用 '?' 进行标记
                pop_prth = stack.pop() if stack else '?'

                # 如果左右括号不成对,直接返回 False
                if prths_mapping[ch] != pop_prth:
                    return False
            else:  # 左括号入栈
                stack.append(ch)
        # stack 最终为空,则表示为有效
        return not stack

LeetCode.合并两个有序链表

题目

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

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

解题思路

  1. 设定“哨兵节点”,用以正确返回合并后的链表;
  2. 控制 cur 指针,调整它的 next 指针;
  3. 比较两个链表的节点大小,将小的节点接在 cur 节点后面;
  4. 循环操作,当其中一个链表为空时终止;
  5. 若循环结束,其中一个链表不为空,则将剩余部分接在合并链表后面(因为两个链表都是有序的,所以剩余部分都比合并链表的元素大;
  6. 最后,返回合并的链表。

代码实现如下:

# # Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
        '''将两个有序链表合并成一个链表然后返回

        Args:
            l1: 有序链表 1
            l2: 有序链表 2
        
        Returns:
            返回合并后的有序链表
        '''
         # 定义一个哨兵节点,用以正确返回合并后的链表
        pre_head = ListNode(None)

        # 控制 cur 指针,比较节点大小
        cur = pre_head

        # 比较两个链表的节点大小,当两个链表任意一个为空,则终止
        while l1 and l2:
            # 当 l1 的节点较小,则指针指向 l1,同时向后移
            if l1.val < l2.val:
                cur.next, l1 = l1, l1.next
            else:  # 否则,指针指向 l2,同时向后移
                cur.next, l2 = l2, l2.next
            # 维护 cur 指针
            cur = cur.next
        # 考虑其中任意一个链表为空,将非空的链表拼接在合并链表后面
        cur.next = l1 if l1 else l2
        return pre_head.next

LeetCode.合并两个有序数组

题目

给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。

说明:

初始化 nums1 和 nums2 的元素数量分别为 m 和 n。

你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。

示例:

输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6],       n = 3

输出: [1,2,2,3,5,6]

解题思路

  1. 采用双指针的方法,从后面往前遍历
  2. 定义 p1 指针指向 num1 数字末尾,p2 指向 num2 数字末尾,p 指向 num1 的最末尾;
  3. 从后往前循环遍历比较两个数组之间元素的大小;
  4. 当 p < 0 时,也就是 p1 和 p2 其中一个小于 0,遍历结束;
  5. 若是 num2 数组有剩余部分,因为数组都是有序的,直接将 num2 数组剩余部分放到 num1 数组对应的位置。

代码实现如下:

class Solution:
    def merge(self, nums1, m: int, nums2, n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        
        合并两个有序数组为一个有序数组

        Args:
            num1: 有序数组 1
            m: num1 的元素数量
            num2: 有序数组 2
            n:num2 的元素数量
        
        Returns:
            返回合并后的有序数组
        """
        # 通过双指针的方法,从后向前遍历的方法、
        # 定义指针 p1 指向 num1 的数字末尾
        # 定义指针 p2 指向 num2 的数字末尾
        # 定义指针 p 指向 num1 的最末尾
        p1 = m - 1
        p2 = n - 1
        p = m + n - 1
        # 当 p 小于 0,即是 p1 和 p2 其中一个小于 0,遍历结束
        while p1 >= 0 and p2 >= 0:
            if nums1[p1] < nums2[p2]:
                nums1[p] = nums2[p2]
                p2 -= 1
            else:
                nums1[p] = nums1[p1]
                p1 -= 1
            p -= 1
        # 两个数组都是有序的
        # 若是 p1 先小于 0,将 num2 剩余部分放在 num1 数组前面
        nums1[:p2 + 1] = nums2[:p2 + 1]

LeetCode.反转链表

题目

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

解题思路

  1. 迭代。利用双指针;
  2. 定义两个指针,一个 pre 指向 None,一个 cur 指向链表头部;
  3. 遍历链表,将当前节点的 next 指针指向 pre,同时 pre 和 cur 指针都往前进一位(这里注意元素保存)
  4. 当 cur 为None,迭代完毕,pre 此时指向链表头部,返回。

代码实现如下:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def reverseList(self, head: ListNode) -> ListNode:
        '''反转链表

        Args:
            head: 链表
        
        Returns:
            返回反转后的链表
        '''
        # 定义两个指针
        # pre 指向 None,cur 指向原链表头部
        pre = None
        cur = head
        # cur 为 None,迭代结束
        while cur:
            # 这里注意引用更改,节点的存储
            # 下面的代码相当于
            # 存储节点值
            # tmp = cur.next
            # 将 cur 的 next 指向 pre
            # cur.next = pre
            # 移动两个指针
            # pre = cur
            # cur = tmp
            cur.next, pre, cur = pre, cur, cur.next
        # 返回反转后的链表
        return pre

LeetCode.最长回文串

题目

给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。

在构造过程中,请注意区分大小写。比如 “Aa” 不能当做一个回文字符串。

注意:
假设字符串的长度不会超过 1010。

示例:

输入:
"abccccdd"

输出:
7

解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。

解题思路

回文串的概念:就是正反读都一样的字符串。

例如:abba,这个字符串是回文串,中心界限是 ab|ba 中间这条竖线的位置。abdba,而这个字符串也是回文串,中心界限在 ab(d)ba 中字符 d 的位置。

在这里我们可以看出,构成回文串,字符个数有可能是奇数,也有可能是偶数。

当构成回文串的字符个数为偶数的情况下,字符一定是成对出现的(如 abba,都是成对存在);当构成回文串的字符个数为奇数的情况下,剔除中间中心的字符,其他的字符也是成对存在的。(如 abdba,剔除中心字符 d,其他字符也是成对)

但是这样需要注意的是,题中所给出的字符,当某个字符个数为奇数的情况下,并不是说不能用来构建回文串,只要剔除一个,变为偶数,那么同样可构成回文串。

当某个字符个数为奇数的情况下,可以保留其中 1 个不剔除,用以作为回文串中心,所以需要看情况在成对字符构成回文串后,将保留的这 1 个字符添加进去。若是字符都是成对的情况,则不需要考虑这种情况。

代码实现如下:

class Solution:
    def longestPalindrome(self, s: str) -> int:
        from collections import Counter
        # 导入 Counter 类,用以统计字母出现的次数
        d = Counter(s)

        ans = len(s)
        # 统计字母出现次数为奇数有多少
        odd = 0

		# 遍历字母对应的个数
        for value in d.values():
        	# 统计字母出现次数为奇数有多少
        	# 先对符合条件的进行统计 + 1
            if value % 2 != 0:
                odd += 1
		# 当 odd 为 0 的情况下
		# 也就是所有字符都成对存在的情况下,直接返回题目所给字符的长度即可
		# 若 odd 不为 0,也就是有出现字母个数为奇数的情况下,
		# 这里的思路是将所有的字符都添加进来,字符出现次数为奇数的情况,进行统计
		# 将这些情况全部剔除,但同时需要保留 1 个字符作为中心,所以最后要 + 1       
        return ans if odd == 0 else ans - odd + 1

LeetCode. 删除排序数组中的重复项

题目

给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

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

示例1:

给定数组 nums = [1,1,2], 

函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 

你不需要考虑数组中超出新长度后面的元素。

示例2:

给定 nums = [0,0,1,1,1,2,2,3,3,4],

函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

解题思路

思路:双指针

这里要注意审题,题目中已经说明,数组是已经排过序的。那么根据这个前提,我们就能够得出一点,如果数组中,有重复项,那么重复项一定是相邻的。

根据这个前提,我们初始化两个指针,一个在前 front,一个在后 tail。front 先移动,如果 nums[front] == nums[tail],表示元素值重复,front 直接向后移动。

如果 nums[front] != nums[tail],那么表示两个元素不再是重复项,此时将 front 指针所对应的元素值复制给 nums[tail] 的下一个元素,也就是 nums[tail+1],同时 tail 也要向前移动 。

重复上面的过程,直到 front 到达数组尾部。

代码实现如下:

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        length = len(nums)
        if length == 0:
            return 0
        
        # 初始化双指针
        # front 在前,tail 在后
        tail = 0
        front = 1
        
        # 遍历数组
        while front < length:
            # 如果两指针所指的元素值不相同,
            # 表示元素不重复,
            # 这时候将 nums[front] 的值复制给 nums[tail+1]
            if nums[front] !=  nums[tail]:
                tail += 1
                nums[tail] = nums[front]
            # front 持续移动
            front += 1

        return tail + 1

LeetCode. 列表全排列

题目

给定一个 没有重复数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

解题思路

思路:深度优化搜索

先看题目,以所给数组 [1, 2, 3] 的全排列为例:

以 1 开始,全排列有:[1,2,3], [1,3,2];
以 2 开始,全排列有 [2,1,3], [2,3,1];
以 3 开始,全排列有 [3,1,2], [3,2,1]。
从上面的情况,可以看出。枚举每个每一位可能出现的情况,已选择的数字在下面的选择则不能出现。按照这个做法,所有的情况将能够罗列出来。这里其实就是执行一次深度优先搜索,从根节点到叶子节点形成的路径就是一个全排列。

按照这种思路,沿用上面的例子,从空列表 [] 开始,以 1 开始为例。现在确定以 1 开始,则列表为 [1],现在选择 [2] 和 [3] 之中的一个,先选 2,最后剩下的只有数字 3,所以形成全排列 [1, 2, 3]。

已知还有一种情况,也就是 [1, 3, 2],那么如何实现从 [1, 2, 3] 到 [1, 3, 2] 的变化。深度优先搜索是如何实现的?其实是从 [1, 2, 3] 回到 [1, 2] 的情况,撤销数字 3,因为当前层只能选择 3,所以再撤销 2 的选择,这样后面的程序则能在选择 3 的时候后续也能选择 2。

代码实现如下:

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:

        def _dfs(nums, depth, pmt, be_selected, length, ans):
            # 表示深度优化搜索的深度等于数组长度时,这个时候表示全排列已经生成
            # 也就是符合情况的选择已经选择完毕
            # 将这个全排列的情况添加到列表中
            # 这里需要注意,pmt 在参数传递是引用传递,拷贝一份添加到结果中
            if depth == length:
                ans.append(pmt[:])
                return

            # 开始遍历
            for i in range(length):
                # be_selected,表示原数组中的元素的状态,是否被选择,是为 True,否为 False
                if not be_selected[i]:
                    # 当元素被选择时,改变状态
                    be_selected[i] = True
                    # 将元素添加到 pmt 中,以构成后续
                    pmt.append(nums[i])
                    # 向下一层进行遍历
                    _dfs(nums, depth + 1, pmt, be_selected, length, ans)
                    # 遍历结束时,进行回溯,这个时候状态要进行重置
                    # 如上面说的 `[1, 2, 3]` 到 `[1, 3, 2]` 中变化,要撤销 3,再撤销 2,重新选择
                    # 状态改变
                    be_selected[i] = False
                    # 撤销
                    pmt.pop()

        length = len(nums)
        if length == 0:
            return []

        be_selected = [False] * length
        ans = []
        _dfs(nums, 0, [], be_selected, length, ans)
        return ans

LeetCode. 数组中数字出现1次的数 

题目

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

示例2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

解题思路

思路:异或,位运算

先说下异或的性质:二进制位同一位相同则为 0,不同则为 1。

再说说一下异或的规律:

若两数值相同,两者的异或结果为 0,(即是任何数与自身异或结果为 0)
任何数与 0 异或结果为本身
同时异或是满足交换律,结合律的(数学符合:⊕)

交换律: a ⊕ b = b ⊕ a
结合律: a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c
回来看本题,题目说明,【整型数组 nums 中,除两个数字之外,其他数字都出现了两次】。那么根据交换律、结合律将相同数字进行异或运算,那么相同数字都会变为 0,再根据第二条规律,那么剩下的出现一次的数字。

代码实现如下:

class Solution:
    def singleNumbers(self, nums: List[int]) -> List[int]:
        res = 0
        # 全员进行异或
        for num in nums:
            res ^= num
        
        # 找出不为 0 的最低位
        # & 位运算的使用
        div = 1
        while (div & res == 0):
            div <<= 1
        
        # 进行分组
        p, q = 0, 0
        for num in nums:
            if num & div:
                p ^= num
            else:
                q ^= num
            
        return [p, q]

LeetCode. K 个一组翻转链表

题目

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

示例:

给你这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5

说明:

  • 你的算法只能使用常数的额外空间。
  • 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

解题思路

思路:迭代、翻转链表

  1. 首先要确保翻转的范围,这个是由题目中提及的 k 来控制;
  2. 关于链表的翻转,要注意前驱和后继的问题,防止指向错误,这里也为了将翻转后的链表与后续进行连接;
  3. 定义 pre 和 tail,pre 表示待翻转链表部分的前驱,tail 表示末尾;
  4. 上面的 tail 经由 k 控制到达末尾;
  5. 翻转链表,将翻转后的部分与后续进行拼接;
  6. 注意:根据题意,当翻转部分的长度小于 k 时,这个时候不做处理。

代码实现如下:

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
        if head == None and head.next == None and k < 2:
            return head
        # 定义哨兵节点
        dummy = ListNode(0)
        # 指向节点
        dummy.next = head

        # 定义前驱后继节点
        pre = dummy
        tail = dummy

        # 控制 tail 到待翻转链表部分的末尾
        while True:
            count = k
            while count > 0 and tail != None:
                count -= 1
                tail = tail.next
            # 到达尾部时,长度不足 k 时,跳出循环
            if tail == None:
                break

            # 这里用于下次循环
            head = pre.next
            # 开始进行翻转
            while pre.next != tail:
                tmp = pre.next
                pre.next = tmp.next
                tmp.next = tail.next
                tail.next = tmp
            
            # 重置指针
            pre = head
            tail = head
        
        return dummy.next

LeetCode. 只出现一次的数字 

题目

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

说明:

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

示例:

输入: [4,1,2,1,2]
输出: 4

解题思路

思路:位运算

说明位运算方法的实现前,我们先看下题目,在说明后面,题目要求【算法应具有线性事件复杂度,不需要额外的空间】。如果没有这个要求,我们能想到方法可能有以下几种:

  1. 使用集合存储,遍历数组,当数字未在集合中时,先添加到集合中,当存在这个数字时,则从集合中删除,因为题目中已经说明,只有 1 个数字时重复的,那么最后集合中剩下的就是该数字。
  2. 使用哈希表存储数字以及数字出现的次数,遍历数组,维护哈希表。最终遍历哈希表,数字出现次数为 1 的就是所求得数字。
  3. (这个思路来自官方题解),同样使用集合存储数字,但是这里涉及到普通的运算。集合存储数组的元素,因为集合中不会出现重复的数字,而数组中只有一个数字是重复的,当集合中的数字和的两倍减去原数组中所有元素之和,那么两者所得的结果就是这个数字。

以上方法都能够解决该问题,但是题意要求【算法应具有线性事件复杂度,不需要额外的空间】。那么这三个方法都不满足条件。

关于位运算的使用,这里再重新说一下异或的性质:二进制位同一位相同则为 0,不同则为 1。关于异或的规律:

任何数与自身异或结果为 0。
任何数与 0 异或结果为本身。
同时,异或满足交换律、结合律(数学符号:⊕)

交换律: a ⊕ b = b ⊕ a
结合律: a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c
先定义返回变量为 0,对数组变量依次进行异或,那么根据上面的规律,我们就可以计算出题目中所说那个不重复的数字。

代码实现如下:

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        # 任何数与 0 异或结果为自身
        # 这里定义变量 0,对数组所有元素进行异或时
        # 任何数与自身异或结果为 0
        # 那么最终剩下的就是单独的数字
        res = 0
        for x in nums:
            res ^= x
        return res

LeetCode. 寻找重复数

题目

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例:

输入: [1,3,4,2,2]
输出: 2

说明:

  • 不能更改原数组(假设数组是只读的)。
  • 只能使用额外的 O(1) 的空间。
  • 时间复杂度小于 O(n2) 。
  • 数组中只有一个重复的数字,但它可能不止重复出现一次。

解题思路

思路:二分查找

这里需要注意,题意中说明,有 4 个提示。这里会限制一些方法,例如:

对数组排序,重复数相邻,根据这个就可以找到重复数(这里违背【不能更改原数组】)
使用哈希表,(这里违背【只能使用额外的 O(1) 算法】)

上面的方法,在没有限制的情况下,可以使用,但是在这里,由于题目给出了限制,所以暂不考虑。

先看本题,【给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n)】,这是题目中给定的前提。

根据这个前提,这里二分法的思路是先定一个数值(这里同样定 [left, right] 的中间值 mid),不过这里要统计的是原始数组中,小于等于这个 mid 的值的元素个数(这里定义为 count)。如果这个 count 严格大于 mid 的值的话,这个重复元素就会落在区间 [left, mid] 中。

这里涉及一个原理:抽屉原理。关于抽屉原理,大致是这样的一个现象:如果桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。

以题目中所给的示例 1 进行展开分析:

[1,3,4,2,2]
1

首先,我们知道,这里的数值 2 是重复值。现在,我们按照上面的思路来分析一下。

这里有 5 个数字,即是 n + 1 = 5,n = 4,那么这里数组的数值都在 1 到 4 之间。

现在先定一个值(根据上面所述的中间值 mid),这里定为 2,遍历数组,统计小于等于 2 的数字个数。这里可以看到,有 3 个值,严格大于 mid,所以重复值落在 [1, 2] 这个区间中。

代码实现如下:

class Solution:
    def findDuplicate(self, nums: List[int]) -> int:
        size = len(nums)

        left = 1
        right = size - 1

        while left < right:
            # 先找中间值
            mid = left + (right - left) // 2
            # mid = (left + right) // 2
            # 遍历数组
            # 统计数组中小于等于中间值的元素个数
            count = 0
            for num in nums:
                if num <= mid:
                    count += 1
            
            # 如果统计数严格大于中间值时,那么重复值将落在 [left, mid] 这个区间
            if count > mid:
                # 将右边界缩小到 mid
                right = mid
            # 否则重复值落在 [mid + 1, right]
            else:
                left = mid + 1
        return left

LeetCode. 最长有效括号

题目

给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。

示例:

输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"

解题思路

思路:栈

题目中,要查找有效括号,有可能需要从内往外扩展,这符合栈先入后出的特性。

我们先使用暴力解,来尝试解决这个问题。

有效的括号是成对出现的,那么我们尝试罗列所有可能的非空偶数长度子字符串,判断其是否有效。

当我们遇到左括号 ( 时,我们将它入栈。当遇到右括号 ) 时,从栈中弹出一个左括号 (,如果栈中没有了左括号,或者遍历完成后栈中还保留有元素,那么这个子字符串就是无效的。循环遍历,更新最大的长度。

代码实现如下:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        max_len = 0
        stack = []
        # 当先遇到 `)` 需要先弹出元素,这里可以防止报错
        # 当遇到的 `()` 的字符时,-1 在计算长度的时候,发挥作用
        stack.append(-1)
        for i in range(len(s)):
            if s[i] == '(':
                stack.append(i)
            else:
                stack.pop()
                if not stack:
                    stack.append(i)
                else:
                    max_len = max(max_len, i - stack[-1])
        return max_len

LeetCode. 判断链表中是否有环

题目

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

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例1:

输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

测试&测开面试要知道的那些事--算法篇(LeetCode_Python)_第1张图片

示例2:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

测试&测开面试要知道的那些事--算法篇(LeetCode_Python)_第2张图片

解题思路

思路:哈希表、快慢指针
在这里,我们直接看示例,如果链表存在环,那么某一个结点被访问的次数将不止一次。

哈希表
那么,我们使用哈希表来存储访问的节点,判读依据如下:

当某个结点在被访问时,发现存在于哈希表中,那么我们可以判断该链表为环形链表。
否则继续访问,直至结点为空。

快慢指针
题目最后进阶部分,提出是否能够用常量空间复杂度解决问题。因为前面的方法,定义哈希表存储需要额外的空间。

在这里,我们用快慢指针的思路来解决。具体如下:

定义两个指针 p、q,其中一个快指针 p 每次移动两步,慢指针 q 每次移动一步;
如果不存在环,那么快指针会先一步到达尾部,那么我们就可以返回 False;
如果存在环,那么快指针将会再次到达链表的某个结点,最终快慢指针将会重合,返回 True。

代码实现如下:

# 哈希表
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        hash_map = {}
        
        # 结点非空时
        while head:
            # 先判断结点是否已经存在于哈希表中
            # 存在,则表示存在环
            if head in hash_map:
                return True
            # 记录访问的节点,访问过都标记为 1
            hash_map[head] = 1
            head = head.next
        
        return False

# 快慢指针
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        # 定义快慢指针
        # 快指针 p 每次移动两步,慢指针 q 每次移动一步
        p = head
        q = head
        
        while p and p.next:
            q = q.next
            p = p.next.next
            if p == q:
                return True
        return False

你可能感兴趣的:(面试,算法,leetcode,面试,python)