前言
本题解Go语言部分基于 LeetCode-Go
其他部分基于本人实践学习
个人题解GitHub连接:LeetCode-Go-Python-Java-C
本文部分内容来自网上搜集与个人实践。如果任何信息存在错误,欢迎读者批评指正。本文仅用于学习交流,不用作任何商业用途。
文章目录
- 前言
- [1. Two Sum](https://leetcode.com/problems/two-sum/)
- [2. Add Two Numbers](https://leetcode.com/problems/add-two-numbers/)
- [3. Longest Substring Without Repeating Characters](https://leetcode.com/problems/longest-substring-without-repeating-characters/)
- [4. Median of Two Sorted Arrays](https://leetcode.com/problems/median-of-two-sorted-arrays/)
- [5. Longest Palindromic Substring](https://leetcode.com/problems/longest-palindromic-substring/)
- [6. ZigZag Conversion](https://leetcode.com/problems/zigzag-conversion/)
- [7. Reverse Integer](https://leetcode.com/problems/reverse-integer/)
1. Two Sum
题目
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
Example:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].
题目大意
在数组中找到 2 个数之和等于给定值的数字,结果返回 2 个数字在数组中的下标。
解题思路
这道题最优的做法时间复杂度是 O(n)。
顺序扫描数组,对每一个元素,在 map 中找能组合给定值的另一半数字,如果找到了,直接返回 2 个数字的下标即可。如果找不到,就把这个数字存入 map 中,等待扫到“另一半”数字的时候,再取出来返回结果。
- 使用map[int]int来保存元素值和索引的映射
- 遍历数组nums,计算目标值与当前元素的差值another
- 在map中查找another是否存在,如果找到则返回两个索引
- 如果map中不存在another,则将当前元素和索引存入map
- 遍历结束后若没有结果则返回nil
Python:
- 使用字典num_map保存元素和索引的映射
- 遍历nums列表,利用enumerate同时获取索引和值
- 计算目标值与当前元素的差值another,判断another是否在num_map中
- 如果在,返回两个元素的索引;如果不在,将当前元素和索引存入num_map
- 遍历结束若无结果则返回空列表[]
Java:
- 使用HashMap保存元素值和索引的映射
- 遍历数组nums,计算目标值与当前元素差值another
- 判断another是否在Map中,如果存在直接返回两个索引
- 如果another不在Map中,将当前元素和索引存入Map
- 遍历结束若无结果则返回空数组new int[0]
C++:
- 使用unordered_map来保存元素值和索引的映射关系。
- 遍历数组nums,对于每个元素,计算目标值与当前元素的差值another。
- 在unordered_map中查找another是否存在,如果存在则返回两个索引。
- 如果unordered_map中不存在another,则将当前元素和索引存入unordered_map。
- 遍历结束后,如果没有找到结果,则返回一个空的结果数组。
Go
func twoSum(nums []int, target int) []int {
// 定义map用于存储元素和索引
m := make(map[int]int)
// 遍历数组
for i := 0; i < len(nums); i++ {
// 计算另一个需要的数
another := target - nums[i]
// 在map中查找another是否存在
if _, ok := m[another]; ok {
// 如果存在,返回两个数的索引
return []int{m[another], i}
}
// 不存在,将当前元素和索引存入map
m[nums[i]] = i
}
// 遍历完成未找到,返回nil
return nil
}
Python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
# 定义字典保存元素和索引映射
num_map = {}
# 遍历nums列表,enumerate可以同时获得迭代对象的索引和值
for i, num in enumerate(nums):
# 计算需要的另一个数
another = target - num
# 判断another是否在num_map中
if another in num_map:
# 如果在,返回两个数的索引
return [num_map[another], i]
# 如果不在,将当前num和索引添加到num_map
num_map[num] = i
# 遍历结束没有找到,返回空列表
return []
Java
class Solution {
public int[] twoSum(int[] nums, int target) {
// 定义HashMap保存数值和索引
Map<Integer, Integer> numMap = new HashMap<>();
// 遍历数组
for (int i = 0; i < nums.length; i++) {
// 计算需要的另一个数
int another = target - nums[i];
// 判断map中是否存在该数
if (numMap.containsKey(another)) {
// 如果存在直接返回两个数的索引
return new int[] {numMap.get(another), i};
}
// 不存在则将当前数和索引放入map
numMap.put(nums[i], i);
}
// 遍历结束没有结果则返回空数组
return new int[0];
}
}
Cpp
#include <vector>
#include <unordered_map>
class Solution {
public:
std::vector<int> twoSum(std::vector<int>& nums, int target) {
std::unordered_map<int, int> m; // 创建一个unordered_map用于存储元素和索引的对应关系
std::vector<int> result;
for (int i = 0; i < nums.size(); i++) {
int another = target - nums[i]; // 计算需要的另一个数
if (m.find(another) != m.end()) { // 在map中查找是否存在该数
result.push_back(m[another]); // 将对应的索引添加到结果数组中
result.push_back(i);
return result;
}
m[nums[i]] = i; // 将当前元素和索引存入map
}
return result; // 遍历完成后仍未找到符合条件的数对,返回空的结果数组
}
};
四个版本两数之和解法所需的基础知识:
Go 版本:
- map:Go中内置的字典类型,用于存储键值对。查找时间复杂度 O(1)。
- 索引访问:Go中的数组可以通过索引快速访问元素。
- _, ok:= map[key] 操作:在map中查找key,ok为bool类型,表示是否找到。
- 语法:条件判断、循环、函数定义、变量声明等语法基础。
Python 版本:
- 字典(dict):Python内置的数据结构,用于存储键值对映射关系。查找速度快,时间复杂度为 O(1)。
- enumerate():Python 的内置函数,可以同时遍历序列的元素和索引。
- list:Python 的列表数据类型,可变序列,支持快速插入和删除。
- in操作符:用于判断字典中是否存在指定的键。
- 语法:条件表达式、循环、函数定义、索引访问等基础语法。
Java 版本:
- HashMap:Java中的哈希表实现,用于存储键值对。查找速度快,时间复杂度为 O(1)。
- 数组和索引:数组int[]存储数据,索引访问元素。
- containsKey():Map中的方法,判断是否包含指定的键。
- 语法:条件判断、循环、方法定义、数组索引等基础语法。
C++版本:
- unordered_map:C++中的哈希表实现,用于存储键值对。查找速度快,时间复杂度为O(1)。
- vector:C++中的动态数组,可变长度的序列容器,支持快速插入和访问元素。
- for循环:C++中的循环结构,用于遍历数组或其他可迭代对象。
- if语句:C++中的条件判断语句,用于根据条件执行不同的代码块。
- 数组索引:C++中使用方括号和索引值来访问数组中的元素。
- 以上是C++版本解题所需的基础知识,这些知识点在解题过程中用于实现对数组和哈希表的访问、遍历和判断操作。
2. Add Two Numbers
题目
You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order
and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.
You may assume the two numbers do not contain any leading zero, except the number 0 itself.
Example:
Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807.
题目大意
2 个逆序的链表,要求从低位开始相加,得出结果也逆序输出,返回值是逆序结果链表的头结点。
解题思路
需要注意的是各种进位问题。
极端情况,例如
Input: (9 -> 9 -> 9 -> 9 -> 9) + (1 -> )
Output: 0 -> 0 -> 0 -> 0 -> 0 -> 1
为了处理方法统一,可以先建立一个虚拟头结点,这个虚拟头结点的 Next 指向真正的 head,这样 head 不需要单独处理,直接 while
循环即可。另外判断循环终止的条件不用是 p.Next != nil,这样最后一位还需要额外计算,循环终止条件应该是 p != nil。
对于Go版本的解题思路:
- 创建一个空节点作为返回链表的头节点。
- 使用变量n1、n2表示链表l1和l2的当前节点值,carry表示当前位的进位,current指向返回链表的当前节点。
- 当l1、l2任意一个链表未遍历完或有进位时,继续循环。
- 如果l1链表已遍历完,将n1设为0;否则取l1当前节点值。
- 对l2链表进行同样处理。
- 求当前位相加结果,新建节点,节点值是当前位求和对10取余的结果。
- current指针前移,计算下一位的进位。
- 返回头节点的下一个节点,作为链表结果。
对于Python版本的解题思路:
- 创建一个空节点作为返回链表的头节点。
- current指向返回链表的当前节点,carry表示当前位的进位。
- 遍历l1和l2链表,同时存在节点或有进位时,继续循环。
- 获取当前节点的值,如果节点不存在则取0。
- 计算当前位的和以及进位,并创建新节点。
- current指针前移,更新l1和l2的指针。
- 返回头节点的下一个节点,作为链表结果。
对于Java版本的解题思路:
- 创建一个空节点作为返回链表的头节点。
- current指向返回链表的当前节点,carry表示当前位的进位。
- 遍历l1和l2链表,同时存在节点或有进位时,继续循环。
- 获取当前节点的值,如果节点不存在则取0。
- 计算当前位的和以及进位,并创建新节点。
- current指针前移,更新l1和l2的指针。
- 返回头节点的下一个节点,作为链表结果。
对于C++版本的解题思路:
- 创建一个空节点作为返回链表的头节点。
- current指向返回链表的当前节点,carry表示当前位的进位。
- 遍历l1和l2链表,同时存在节点或有进位时,继续循环。
- 获取当前节点的值,如果节点不存在则取0。
- 计算当前位的和以及进位,并创建新节点。
- current指针前移,更新l1和l2的指针。
- 返回头节点的下一个节点,作为链表结果。
Go
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode {
// 创建一个空节点作为返回链表的头节点
head := &ListNode{Val: 0}
// 定义变量 n1、n2 表示链表 l1 和 l2 的当前节点值
// carry 表示当前位的进位
// current 指向返回链表的当前节点
n1, n2, carry, current := 0, 0, 0, head
// 当 l1、l2 任意一个链表未遍历完 或 有进位时,继续循环
for l1 != nil || l2 != nil || carry != 0 {
// 如果 l1 链表已遍历完,将 n1 设为 0
if l1 == nil {
n1 = 0
} else {
// 否则取 l1 当前节点值
n1 = l1.Val
// l1 指针前移
l1 = l1.Next
}
// 对 l2 链表进行同样处理
if l2 == nil {
n2 = 0
} else {
n2 = l2.Val
l2 = l2.Next
}
// 求当前位相加结果
// 新建节点,节点值是当前位求和对 10 取余的结果
current.Next = &ListNode{Val: (n1 + n2 + carry) % 10}
// current 指针前移
current = current.Next
// 计算下一位的进位
carry = (n1 + n2 + carry) / 10
}
// 返回头节点的下一个节点,作为链表结果
return head.Next
}
Python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
head = ListNode() # 创建一个空节点作为返回链表的头节点
current = head # current指向返回链表的当前节点
carry = 0 # carry表示当前位的进位
while l1 or l2 or carry:
n1 = l1.val if l1 else 0
n2 = l2.val if l2 else 0
sum = n1 + n2 + carry
carry = sum // 10 # 计算下一位的进位
current.next = ListNode(sum % 10) # 新节点,节点值是当前位求和对10取余的结果
current = current.next # current指针前移
if l1:
l1 = l1.next # l1指针前移
if l2:
l2 = l2.next # l2指针前移
return head.next # 返回头节点的下一个节点,作为链表结果
Java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = new ListNode(); // 创建一个空节点作为返回链表的头节点
ListNode current = head; // current指向返回链表的当前节点
int carry = 0; // carry表示当前位的进位
while (l1 != null || l2 != null || carry != 0) {
int n1 = (l1 != null) ? l1.val : 0;
int n2 = (l2 != null) ? l2.val : 0;
int sum = n1 + n2 + carry;
carry = sum / 10; // 计算下一位的进位
current.next = new ListNode(sum % 10); // 新建节点,节点值是当前位求和对10取余的结果
current = current.next; // current指针前移
if (l1 != null) l1 = l1.next; // l1指针前移
if (l2 != null) l2 = l2.next; // l2指针前移
}
return head.next; // 返回头节点的下一个节点,作为链表结果
}
}
Cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
#include <iostream>
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* head = new ListNode(); // 创建一个空节点作为返回链表的头节点
ListNode* current = head; // current指向返回链表的当前节点
int carry = 0; // carry表示当前位的进位
while (l1 != nullptr || l2 != nullptr || carry != 0) {
int n1 = (l1 != nullptr) ? l1->val : 0;
int n2 = (l2 != nullptr) ? l2->val : 0;
int sum = n1 + n2 + carry;
carry = sum / 10; // 计算下一位的进位
current->next = new ListNode(sum % 10); // 新建节点,节点值是当前位求和对10取余的结果
current = current->next; // current指针前移
if (l1 != nullptr) l1 = l1->next; // l1指针前移
if (l2 != nullptr) l2 = l2->next; // l2指针前移
}
return head->next; // 返回头节点的下一个节点,作为链表结果
}
};
对于Go版本,你需要掌握以下基础知识:
- 了解链表的概念和基本操作,例如遍历链表和创建新节点。
- 熟悉Go语言的基本语法和数据类型,包括变量声明、条件语句、循环语句等。
- 理解函数的定义和调用,以及函数参数和返回值的使用。
- 熟悉指针的概念和用法,因为链表节点通常使用指针来连接。
对于Python版本,你需要掌握以下基础知识:
- 理解链表的概念和基本操作,包括遍历链表和创建新节点。
- 熟悉Python的基本语法和数据类型,例如变量、条件语句、循环语句等。
- 熟悉类的定义和使用,因为链表节点可以使用类来表示。
- 理解函数的定义和调用,以及函数参数和返回值的使用。
对于Java版本,你需要掌握以下基础知识:
- 理解链表的概念和基本操作,例如遍历链表和创建新节点。
- 熟悉Java的基本语法和数据类型,包括变量声明、条件语句、循环语句等。
- 熟悉类的定义和使用,因为链表节点可以使用类来表示。
- 理解函数的定义和调用,以及函数参数和返回值的使用。
对于C++版本,你需要掌握以下基础知识:
- 理解链表的概念和基本操作,包括遍历链表和创建新节点。
- 熟悉C++的基本语法和数据类型,例如变量声明、条件语句、循环语句等。
- 熟悉类的定义和使用,因为链表节点可以使用类来表示。
- 理解指针的概念和用法,因为链表节点通常使用指针来连接。
3. Longest Substring Without Repeating Characters
题目
Given a string, find the length of the longest substring without repeating characters.
Example 1:
Input: "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3.
Example 2:
Input: "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.
Example 3:
Input: "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.
Note that the answer must be a substring, "pwke" is a subsequence and not a substring.
题目大意
在一个字符串中寻找没有重复字母的最长子串。
解题思路
这一题和第 438 题,第 3 题,第 76 题,第 567 题类似,用的思想都是"滑动窗口"。
滑动窗口的右边界不断的右移,只要没有重复的字符,就持续向右扩大窗口边界。一旦出现了重复字符,就需要缩小左边界,直到重复的字符移出了左边界,然后继续移动滑动窗口的右边界。以此类推,每次移动需要计算当前长度,并判断是否需要更新最大长度,最终最大的值就是题目中的所求。
Go
解法一:滑动窗口 + Map
- 使用map[byte]int记录每个字符最后出现的索引
- 初始化左右指针和结果res
- 右指针遍历字符串,若字符的索引大于等于左指针索引,左指针移到重复字符下一位
- 更新字符的索引,计算窗口大小更新结果
- 返回结果
解法二:滑动窗口 + 数组
- 定义int数组记录每个字符的出现频次
- 初始化左右指针和结果res
- 右指针右移,若字符频次为0,加1
- 否则左指针右移,对应频次减1
- 计算窗口大小更新结果
- 返回结果
解法三:滑动窗口 + Bitmap
- 定义一个数组作为Bitmap
- 初始化左右指针和结果res
- 右指针右移,若该字符已被标记,左指针右移直到标记重置
- 标记右指针字符,计算窗口大小更新结果
- 返回结果
Python
解法一:滑动窗口 + 集合
- 使用集合来判断字符是否重复出现过。
- 左右指针初始化在字符串开头。
- 右指针不断右移,若字符已在集合中,则移除左指针位置字符,左指针右移。
- 将右指针字符添加到集合,并计算当前窗口大小,更新结果。
- 返回最大窗口大小。
解法二:滑动窗口 + 字符频次数组
- 定义一个数组记录每个ASCII字符出现的频次。
- 初始化左右指针在字符串开头。
- 右指针右移,若字符频次大于0,表示重复,左指针右移同时对应频次-1。
- 右指针字符对应频次+1,并更新结果。
- 返回最大窗口大小。
解法三:滑动窗口 + 哈希表
- 使用哈希表存储字符的最近出现下标。
- 初始化左右指针在字符串开头。
- 右指针右移,若字符在表中且下标>=左指针,左指针移到重复字符处。
- 更新字符的最近下标,计算窗口大小并更新结果。
- 返回最大窗口大小。
解法四:位图
- 初始化一个256位的位图,可以映射ASCII字符
- 左右指针初始化在字符串开头
- 右指针右移,如果字符在位图中已被标记,则重置左指针字符,左指针右移
- 将右指针字符在位图中标记
- 计算窗口大小并更新结果
- 返回最大窗口大小
Java
解法一:滑动窗口 + 位图
- 使用一个boolean数组作为位图来记录每个字符是否出现
- 定义左右指针和结果res,初始化为0
- 右指针右移,如果对应字符已被标记,左指针右移直到重复字符被重置
- 设置右指针字符在位图中为true
- 计算窗口大小更新结果
- 返回最大窗口大小
解法二:滑动窗口 + 数组 - 定义一个int数组记录每个字符的出现频次
- 初始化左右指针和结果res
- 右指针右移,如果字符频次为0,频次加1
- 如果频次大于0,左指针右移同时对应频次减1
- 计算窗口大小更新结果
- 返回最大窗口大小
解法三:滑动窗口 + 哈希表 - 使用HashMap存储字符及其最后出现的索引
- 初始化左右指针和结果res
- 左指针遍历字符串,如果字符在表中且索引>=右指针,移动右指针
- 更新字符的最近索引,计算窗口大小更新结果
- 返回最大窗口大小
C++
解法一:滑动窗口 + 位图
- 定义一个bool数组作为位图
- 初始化左右指针和结果res
- 右指针遍历,若字符已被标记,左指针右移直到该字符标记重置
- 标记右指针字符,计算窗口大小更新结果
- 返回结果
解法二:滑动窗口 + 数组
- 定义一个int数组记录每个字符的出现频次
- 初始化左右指针和结果res
- 右指针右移,若字符频次为0,频次加1
- 否则左指针右移,对应频次减1
- 计算窗口大小更新结果
- 返回结果
解法三:滑动窗口 + 哈希表
- 使用unordered_map存储字符及最后出现的索引
- 初始化左右指针和结果res
- 左指针遍历,如果字符在表中且索引>=右指针,右指针移至重复字符下一位
- 更新字符索引,计算窗口大小更新结果
- 返回结果
Go
// 解法一 位图
func lengthOfLongestSubstring(s string) int {
if len(s) == 0 { // 如果传入的字符串长度为0,直接返回0
return 0
}
var bitSet [256]bool // 定义一个256位的布尔数组,用来标记每个字符是否出现过
result, left, right := 0, 0, 0 // 定义结果、左指针、右指针,初始化为0
for left < len(s) {
// 如果当前右指针指向的字符在位图中被标记为true,说明该字符重复出现过
// 需要左指针向右移动,直到将重复的字符的对应位标记为false
if bitSet[s[right]] {
bitSet[s[left]] = false
left++
} else { // 如果没有重复,将该字符对应位标记为true
bitSet[s[right]] = true
right++
}
// 计算当前窗口大小,更新结果
if result < right-left {
result = right - left
}
// 如果左指针到达字符串结尾,或者右指针超过了字符串长度,结束循环
if left+result >= len(s) || right >= len(s) {
break
}
}
return result // 返回结果
}
// 解法二 滑动窗口
func lengthOfLongestSubstring(s string) int {
if len(s) == 0 { // 字符串为空,返回0
return 0
}
var freq [127]int // 定义一个数组,记录ASCII字符出现的频率
result, left, right := 0, 0, -1
for left < len(s) {
// 如果右指针还可以移动,且对应的字符频率为0,右指针右移
if right+1 < len(s) && freq[s[right+1]] == 0 {
freq[s[right+1]]++
right++
} else { // 否则左指针右移,对应字符频率减1
freq[s[left]]--
left++
}
// 计算当前窗口大小,更新结果
result = max(result, right-left+1)
}
return result
}
func max(a int, b int) int { // 比较两个数大小,返回较大值
if a > b {
return a
}
return b
}
// 解法三 滑动窗口-哈希桶
func lengthOfLongestSubstring(s string) int {
right, left, res := 0, 0, 0 // 初始化右指针、左指针和结果
indexes := make(map[byte]int, len(s)) // map存储每个字符最后出现的index
for left < len(s) {
if idx, ok := indexes[s[left]]; ok && idx >= right { // 如果该字符已存在且在窗口中
right = idx + 1 // 右指针移动到重复字符处
}
indexes[s[left]] = left // 更新字符索引
left++ // 左指针右移
res = max(res, left-right) // 计算窗口大小,更新结果
}
return res
}
func max(a int, b int) int {
if a > b {
return a
}
return b
}
Python
# 解法一:滑动窗口 + 集合
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if len(s) == 0:
return 0 # 字符串为空,直接返回0
char_set = set() # 定义一个集合,记录每个字符是否出现过
l = 0
res = 0
for r in range(len(s)):
while s[r] in char_set: # 如果右指针指向的字符已在集合中
char_set.remove(s[l]) # 从集合中删除左指针位置字符
l += 1 # 左指针右移
char_set.add(s[r]) # 将右指针指向的字符添加到集合
res = max(res, r-l+1) # 计算当前窗口大小,更新结果
return res
# 解法二:滑动窗口 + 字符频次数组
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
if len(s) == 0:
return 0 # 字符串为空,直接返回0
freq = [0] * 128 # 定义数组,记录每个ASCII字符出现的频率
l = 0
res = 0
for r in range(len(s)):
while freq[ord(s[r])] > 0: # 如果右指针字符频率大于0,表示重复
freq[ord(s[l])] -= 1 # 左指针位置字符频率减1
l += 1 # 左指针右移
freq[ord(s[r])] += 1 # 右指针字符频率加1
res = max(res, r-l+1) # 计算窗口大小,更新结果
return res
# 解法三:滑动窗口 + 哈希表
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
l = 0
res = 0
index = {} # 字典存储字符最后出现的index
for r in range(len(s)):
if s[r] in index and index[s[r]] >= l: # 字符已存在且在窗口中
l = index[s[r]] + 1 # 左指针移动到重复字符处
index[s[r]] = r # 更新字符index
res = max(res, r-l+1) # 计算窗口大小,更新结果
return res
# 解法四:位图
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
bitmap = [False] * 256 # 初始化一个256位的位图
l = 0
res = 0
for r in range(len(s)):
while bitmap[ord(s[r])]: # 右指针字符已被标记
bitmap[ord(s[l])] = False # 重置左指针字符
l += 1
bitmap[ord(s[r])] = True # 标记右指针字符
res = max(res, r - l + 1) # 更新结果
return res
Java
class Solution {
// 解法一:位图
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0) {
return 0;
}
// 使用一个位图数组来标记字符是否出现过
boolean[] bitSet = new boolean[256];
int result = 0;
int left = 0, right = 0;
while (left < s.length()) {
// 如果右侧字符对应的bitSet被标记为true,说明此字符在X位置重复,需要左侧���前移动,直到将X标记为false
if (bitSet[s.charAt(right)]) {
bitSet[s.charAt(left)] = false;
left++;
} else {
bitSet[s.charAt(right)] = true;
right++;
}
if (result < right - left) {
result = right - left;
}
// 如果左侧加上结果大于等于字符串长度,或者右侧超过字符串长度,跳出循环
if (left + result >= s.length() || right >= s.length()) {
break;
}
}
return result;
}
}
class Solution {
// 解法二:滑动窗口
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0) {
return 0;
}
int[] freq = new int[127];
int result = 0, left = 0, right = -1;
while (left < s.length()) {
if (right + 1 < s.length() && freq[s.charAt(right + 1)] == 0) {
freq[s.charAt(right + 1)]++;
right++;
} else {
freq[s.charAt(left)]--;
left++;
}
result = Math.max(result, right - left + 1);
}
return result;
}
}
class Solution {
// 解法三:滑动窗口-哈希桶
public int lengthOfLongestSubstring(String s) {
int right = 0, left = 0, res = 0;
Map<Character, Integer> indexes = new HashMap<>();
while (left < s.length()) {
if (indexes.containsKey(s.charAt(left)) && indexes.get(s.charAt(left)) >= right) {
right = indexes.get(s.charAt(left)) + 1;
}
indexes.put(s.charAt(left), left);
left++;
res = Math.max(res, left - right);
}
return res;
}
}
Cpp
#include <unordered_map>
#include <algorithm>
class Solution {
public:
// 解法一:位图
int lengthOfLongestSubstring(string s) {
if (s.length() == 0) {
return 0;
}
// 使用一个位图数组来标记字符是否出现过
bool bitSet[256] = {false};
int result = 0;
int left = 0, right = 0;
while (left < s.length()) {
// 如果右侧字符对应的bitSet被标记为true,说明此字符在X位置重复,需要左侧向前移动,直到将X标记为false
if (bitSet[s[right]]) {
bitSet[s[left]] = false;
left++;
} else {
bitSet[s[right]] = true;
right++;
}
if (result < right - left) {
result = right - left;
}
// 如果左侧加上结果大于等于字符串长度,或者右侧超过字符串长度,跳出循环
if (left + result >= s.length() || right >= s.length()) {
break;
}
}
return result;
}
};
#include <unordered_map>
#include <algorithm>
class Solution {
public:
// 解法二:滑动窗口
int lengthOfLongestSubstring(string s) {
if (s.length() == 0) {
return 0;
}
int freq[127] = {0};
int result = 0, left = 0, right = -1;
while (left < s.length()) {
if (right + 1 < s.length() && freq[s[right + 1]] == 0) {
freq[s[right + 1]]++;
right++;
} else {
freq[s[left]]--;
left++;
}
result = max(result, right - left + 1);
}
return result;
}
};
#include <unordered_map>
#include <algorithm>
class Solution {
public:
// 解法三:滑动窗口-哈希桶
int lengthOfLongestSubstring(string s) {
int right = 0, left = 0, res = 0;
unordered_map<char, int> indexes;
while (left < s.length()) {
if (indexes.count(s[left]) && indexes[s[left]] >= right) {
right = indexes[s[left]] + 1;
}
indexes[s[left]] = left;
left++;
res = max(res, left - right);
}
return res;
}
};
Go
- Map:
- 知道内置map的声明方式,及使用make初始化
- 熟练通过map[key] = val方式设置键值对
- 理解遍历需要通过range迭代map
- 数组:
- 完全掌握数组的定义、初始化、读取、修改等操作
- 注意数组不能负索引,要避免越界访问
- Bitmap:
- 掌握基于数组实现位图,通过索引设置和读取不同bit
- 需要掌握位运算在位图中的应用
- 滑动窗口:
- 理解滑动窗口模板,维护左右指针确定窗口范围
- 在窗口内需要实现问题的具体逻辑
Python
- 字典:
- 完全掌握字典的初始化、添加、删除、修改键值对
- 知道in操作可以检查键是否存在
- 需要掌握遍历字典的items()和keys()方法
- 列表:
- 熟练列表的定义、索引、切片、遍历等操作
- 知道append、insert、pop、remove等修改列表的方法
- 注意索引不能为负,避免索引越界错误
- 位图:
- 掌握基于列表实现位图,通过索引设置和获取不同bit
- 需要熟练使用按位与或非来判断位是否置位
- 滑动窗口:
- 理解模板,使用左右指针维护一个滑动窗口
- 在窗口范围内实现问题的具体逻辑
Java
- HashMap:
- 了解HashMap的put()方法可以添加键值对,get()方法可以获取键对应的值
- 知道containsKey()可以检查键是否存在,containsValue()检查值是否存在
- 需要掌握遍历HashMap的两种方式:使用keySet()或者entrySet()
- 位图:
- 掌握使用boolean类型的数组可以作为位图,通过索引设置和读取不同位
- 需要熟练位图的与或非逻辑运算,用于判断一个位是否被设置
- 数组:
- 完全掌握数组的声明、初始化、读取、修改操作
- 知道数组边界,避免出现索引越界的异常情况
- 滑动窗口:
- 掌握滑动窗口的模板,需要维护左右边界指针
- 理解右指针用于扩大窗口,左指针用于缩小窗口
- 需要在滑动窗口内完成求解问题的相关逻辑
C++
- unordered_map:
- 知道unordered_map的基本语法,使用[]或者insert()添加元素
- 了解find()、count()等查询和统计方法
- 掌握使用迭代器遍历unordered_map
- bitset:
- 完全掌握bitset的定义方法,以及set(),reset(),flip()等修改位的方法
- 需要熟练使用[]方式读取位图中的一个位
- 理解任意位运算,以及与运算判断一个位是否被设置
- 数组:
- 完全掌握数组的定义、初始化、读取、遍历等各种操作
- 注意数组访问需要检查边界,避免越界访问异常
- 滑动窗口:
- 掌握模板,维护左右指针实现窗口的扩大和缩小
- 需要在滑动窗口逻辑内完成问题的求解
4. Median of Two Sorted Arrays
题目
There are two sorted arraysnums1andnums2of size m and n respectively.
Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).
You may assumenums1andnums2cannot be both empty.
Example 1:
nums1 = [1, 3]
nums2 = [2]
The median is 2.0
Example 2:
nums1 = [1, 2]
nums2 = [3, 4]
The median is (2 + 3)/2 = 2.5
题目大意
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
解题思路
-
给出两个有序数组,要求找出这两个数组合并以后的有序数组中的中位数。要求时间复杂度为 O(log (m+n))。
-
这一题最容易想到的办法是把两个数组合并,然后取出中位数。但是合并有序数组的操作是
O(m+n)
的,不符合题意。看到题目给的log
的时间复杂度,很容易联想到二分搜索。 -
由于要找到最终合并以后数组的中位数,两个数组的总大小也知道,所以中间这个位置也是知道的。只需要二分搜索一个数组中切分的位置,另一个数组中切分的位置也能得到。为了使得时间复杂度最小,所以二分搜索两个数组中长度较小的那个数组。
-
关键的问题是如何切分数组 1 和数组 2 。其实就是如何切分数组 1 。先随便二分产生一个
midA
,切分的线何时算满足了中位数的条件呢?即,线左边的数都小于右边的数,即,nums1[midA-1] ≤ nums2[midB] && nums2[midB-1] ≤ nums1[midA]
。如果这些条件都不满足,切分线就需要调整。如果nums1[midA] < nums2[midB-1]
,说明midA
这条线划分出来左边的数小了,切分线应该右移;如果nums1[midA-1] > nums2[midB]
,说明 midA
这条线划分出来左边的数大了,切分线应该左移。经过多次调整以后,切分线总能找到满足条件的解。 -
假设现在找到了切分的两条线了,
数组 1
在切分线两边的下标分别是midA - 1
和midA
。数组 2
在切分线两边的下标分别是midB - 1
和midB
。最终合并成最终数组,如果数组长度是奇数,那么中位数就是max(nums1[midA-1], nums2[midB-1])
。如果数组长度是偶数,那么中间位置的两个数依次是:max(nums1[midA-1], nums2[midB-1])
和min(nums1[midA], nums2[midB])
,那么中位数就是(max(nums1[midA-1], nums2[midB-1]) + min(nums1[midA], nums2[midB])) / 2
。图示见下图:
各语言版本的解题思路:
Go版本:
- 定义一个float64切片nums来存放输入的数字
- 循环遍历nums,计算每个数的32位二进制表示
- 统计每个位上0和1的数量,如果0比1多,则该位全部清0,否则该位全部置1
- 得到修改后的32位二进制数,转为10进制浮点数并存入结果res切片
Python版本:
- 定义一个nums列表存放输入数字
- 遍历nums中的每个数
- 对每个数,获取它的32位二进制形式,统计每个位上0和1的数量
- 如果0的个数多,该位赋值为0,否则赋值为1
- 将修改后的32位二进制数转换为10进制浮点数,添加到结果列表res中
Java版本:
- 定义一个数组nums来存放输入的数字
- 遍历nums,对每个数字求它的32位二进制表示
- 统计每个位上0和1的个数,如果0较多则该位赋0,否则赋1
- 根据修改后的二进制数组生成十进制浮点数
- 将浮点数添加到结果数组res中并返回
C++版本:
- 定义vector nums来存输入数字
- 遍历nums,对每个数字转二进制并统计每位上的0、1数量
- 如果0的数量多,该位赋0,否则赋1
- 将修改后的二进制数转十进制浮点数
- 将浮点数添加到结果vector res中
Go
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
// 假设 nums1 的长度小
if len(nums1) > len(nums2) {
return findMedianSortedArrays(nums2, nums1) // 如果nums1更长,交换参数位置,递归调用
}
low, high, k, nums1Mid, nums2Mid := 0, len(nums1), (len(nums1)+len(nums2)+1)>>1, 0, 0 // low和high表示二分查找的下界和上界,k表示两数组总长度的中位数索引,nums1Mid和nums2Mid表示nums1和nums2的中位数索引
for low <= high {
// nums1: .................. nums1[nums1Mid-1] | nums1[nums1Mid] ........................
// nums2: .................. nums2[nums2Mid-1] | nums2[nums2Mid] ........................
nums1Mid = low + (high-low)>>1 // 分界限右侧是mid,分界线左侧是mid - 1,二分查找更新nums1的中位数索引
nums2Mid = k - nums1Mid // 根据nums1的中位数索引推算出nums2的中位数索引
if nums1Mid > 0 && nums1[nums1Mid-1] > nums2[nums2Mid] {
// nums1中的中位数划分偏右,需要左移
high = nums1Mid - 1
} else if nums1Mid != len(nums1) && nums1[nums1Mid] < nums2[nums2Mid-1] {
// nums1中的中位数划分偏左,需要右移
low = nums1Mid + 1
} else {
// 找到合适的划分,可以输出结果了
break
}
}
midLeft, midRight := 0, 0
if nums1Mid == 0 {
// nums1划分到左边界,取nums2中位数左边那个数
midLeft = nums2[nums2Mid-1]
} else if nums2Mid == 0 {
// nums2划分到左边界,取nums1中位数左边那个数
midLeft = nums1[nums1Mid-1]
} else {
// 否则取两个中位数左边的最大值
midLeft = max(nums1[nums1Mid-1], nums2[nums2Mid-1])
}
if (len(nums1)+len(nums2))&1 == 1 {
// 如果奇数长度,中位数就是midLeft
return float64(midLeft)
}
if nums1Mid == len(nums1) {
// nums1划分到右边界,取nums2中位数右边那个数
midRight = nums2[nums2Mid]
} else if nums2Mid == len(nums2) {
// nums2划分到右边界,取nums1中位数右边那个数
midRight = nums1[nums1Mid]
} else {
// 否则取两个中位数右边的最小值
midRight = min(nums1[nums1Mid], nums2[nums2Mid])
}
return float64(midLeft+midRight) / 2 // 如果偶数长度,中位数是midLeft和midRight的平均值
}
func max(a int, b int) int {
if a > b {
return a
}
return b
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
Python
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
# 确保nums1更短
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
# 初始化变量
low, high = 0, len(nums1)
k = (len(nums1) + len(nums2) + 1) // 2
while low <= high:
# 计算nums1中的中间位置
nums1_mid = low + (high - low) // 2
# 计算nums2中的中间位置
nums2_mid = k - nums1_mid
# 判断是否找到正确的划分
if nums1_mid > 0 and nums1[nums1_mid - 1] > nums2[nums2_mid]:
# nums1中位数划分过大,向左移动
high = nums1_mid - 1
elif nums1_mid < len(nums1) and nums1[nums1_mid] < nums2[nums2_mid - 1]:
# nums1中位数划分过小,向右移动
low = nums1_mid + 1
else:
# 找到正确的划分,计算中位数
break
# 计算中位数
mid_left = 0
if nums1_mid == 0:
mid_left = nums2[nums2_mid - 1]
elif nums2_mid == 0:
mid_left = nums1[nums1_mid - 1]
else:
mid_left = max(nums1[nums1_mid - 1], nums2[nums2_mid - 1])
if (len(nums1) + len(nums2)) % 2 == 1:
return mid_left
mid_right = 0
if nums1_mid == len(nums1):
mid_right = nums2[nums2_mid]
elif nums2_mid == len(nums2):
mid_right = nums1[nums1_mid]
else:
mid_right = min(nums1[nums1_mid], nums2[nums2_mid])
return (mid_left + mid_right) / 2
Java
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 假设 nums1 长度小于等于 nums2
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}
int low = 0;
int high = nums1.length;
int k = (nums1.length + nums2.length + 1) / 2;
int nums1Mid = 0;
int nums2Mid = 0;
while (low <= high) {
// nums1: 左子数组.......... nums1[nums1Mid-1] | nums1[nums1Mid]..........右子数组
// nums2: 左子数组.......... nums2[nums2Mid-1] | nums2[nums2Mid]..........右子数组
nums1Mid = (low + high) / 2;
nums2Mid = k - nums1Mid;
if (nums1Mid > 0 && nums1[nums1Mid - 1] > nums2[nums2Mid]) {
// nums1 中的分界线划多了,要向左边移动
high = nums1Mid - 1;
} else if (nums1Mid != nums1.length && nums1[nums1Mid] < nums2[nums2Mid - 1]) {
// nums1 中的分界线划少了,要向右边移动
low = nums1Mid + 1;
} else {
// 找到合适的划分,可以输出结果
break;
}
}
// 计算中位数
int midLeft = 0, midRight = 0;
if (nums1Mid == 0) {
midLeft = nums2[nums2Mid - 1];
} else if (nums2Mid == 0) {
midLeft = nums1[nums1Mid - 1];
} else {
midLeft = Math.max(nums1[nums1Mid - 1], nums2[nums2Mid - 1]);
}
if ((nums1.length + nums2.length) % 2 == 1) {
// 奇数的情况下中位数就是 midLeft
return midLeft;
}
// 偶数的情况下需要计算 midRight
if (nums1Mid == nums1.length) {
midRight = nums2[nums2Mid];
} else if (nums2Mid == nums2.length) {
midRight = nums1[nums1Mid];
} else {
midRight = Math.min(nums1[nums1Mid], nums2[nums2Mid]);
}
return (midLeft + midRight) / 2.0;
}
}
Cpp
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 假设nums1数组更短
if (nums1.size() > nums2.size()) {
return findMedianSortedArrays(nums2, nums1); // 如果nums1更长,交换两个数组作为参数,递归调用
}
int low = 0; // 二分查找左边界
int high = nums1.size(); // 二分查找右边界
int k = (nums1.size() + nums2.size() + 1) / 2; // 两个数组总长度的中位数索引
int nums1Mid, nums2Mid; // nums1和nums2数组的中位数索引
while(low <= high) {
nums1Mid = low + (high - low) / 2; // 计算nums1数组的中位数索引
nums2Mid = k - nums1Mid; // 根据nums1的中位数索引计算出nums2中的中位数索引
if (nums1Mid > 0 && nums1[nums1Mid-1] > nums2[nums2Mid]) {
// nums1中的中位数划分偏右,需要左移边界
high = nums1Mid - 1;
} else if (nums1Mid != nums1.size() && nums1[nums1Mid] < nums2[nums2Mid-1]) {
// nums1中的中位数划分偏左,需要右移边界
low = nums1Mid + 1;
} else {
// 找到合适的划分,退出二分查找循环
break;
}
}
int midLeft, midRight; // 两个中位数的值
if (nums1Mid == 0) {
// nums1数组划分到最左,取nums2数组中位数左边的值
midLeft = nums2[nums2Mid-1];
} else if (nums2Mid == 0) {
// nums2数组划分到最左,取nums1数组中位数左边的值
midLeft = nums1[nums1Mid-1];
} else {
// 否则取两个数组中位数左边的最大值
midLeft = max(nums1[nums1Mid-1], nums2[nums2Mid-1]);
}
if ((nums1.size() + nums2.size()) % 2 == 1) {
// 如果总长度是奇数,中位数就是midLeft
return midLeft;
}
if (nums1Mid == nums1.size()) {
// nums1数组划分到最右,取nums2数组中位数右边的值
midRight = nums2[nums2Mid];
} else if (nums2Mid == nums2.size()) {
// nums2数组划分到最右,取nums1数组中位数右边的值
midRight = nums1[nums1Mid];
} else {
// 否则取两个数组中位数右边的最小值
midRight = min(nums1[nums1Mid], nums2[nums2Mid]);
}
return (midLeft + midRight) / 2.0; // 如果总长度是偶数,返回平均值
}
};
四个版本解法所需的基础知识:
Go版本:
- 切片的定义、初始化,使用len获取长度,通过下标访问元素
- 移位运算符像<<、>>,与运算符&用于获取特定bit
- math包下的Abs、Pow、Sqrt等数学函数
- 浮点数的基本运算 + - * /
- float64()将整数转为浮点数
Python版本:
- list的定义、初始化,使用len获取长度,通过下标访问元素
- 整数除法//和求余运算%
- 内置函数max、min求最大最小值
- 浮点数的基本运算 + - * /
- float()将整数转浮点数
Java版本:
- 数组的定义、初始化,使用length获取长度,通过下标访问元素
- Math类的静态方法max、min求最大最小值
- 静态方法Math.abs获取绝对值
- 强制类型转换(double)将整数转为双精度浮点数
- 浮点数的基本运算 + - * /
C++版本:
- vector的定义、初始化,size() 获取元素数量,通过下标访问元素
- 整数相除的运算符/和求余运算%
- algorithm头文件下的max、min模板函数求最大最小值
- cmath头文件下的abs等数学函数
- 浮点数的基本运算 + - * /
- 静态强制类型转换static_cast将整数转为双精度浮点数
5. Longest Palindromic Substring
题目
Given a strings
, returnthe longest palindromic substringins
.
Example 1:
Input: s = "babad"
Output: "bab"
Note: "aba" is also a valid answer.
Example 2:
Input: s = "cbbd"
Output: "bb"
Example 3:
Input: s = "a"
Output: "a"
Example 4:
Input: s = "ac"
Output: "a"
Constraints:
1 <= s.length <= 1000
s
consist of only digits and English letters (lower-case and/or upper-case),
题目大意
给你一个字符串 s
,找到 s
中最长的回文子串。
解题思路
- 此题非常经典,并且有多种解法。
- 解法一,动态规划。定义
dp[i][j]
表示从字符串第i
个字符到第j
个字符这一段子串是否是回文串。由回文串的性质可以得知,回文串去掉一头一尾相同的字符以后,剩下的还是回文串。所以状态转移方程是dp[i][j] = (s[i] == s[j]) && ((j-i < 3) || dp[i+1][j-1])
,注意特殊的情况,j - i == 1
的时候,即只有 2 个字符的情况,只需要判断这 2 个字符是否相同即可。j - i == 2
的时候,即只有
3 个字符的情况,只需要判断除去中心以外对称的 2 个字符是否相等。每次循环动态维护保存最长回文串即可。时间复杂度 O(n^2)
,空间复杂度 O(n^2)。
解法二,中心扩散法。动态规划的方法中,我们将任意起始,终止范围内的字符串都判断了一遍。其实没有这个必要,如果不是最长回文串,无需判断并保存结果。所以动态规划的方法在空间复杂度上还有优化空间。判断回文有一个核心问题是找到“轴心”。如果长度是偶数,那么轴心是中心虚拟的,如果长度是奇数,那么轴心正好是正中心的那个字母。中心扩散法的思想是枚举每个轴心的位置。然后做两次假设,假设最长回文串是偶数,那么以虚拟中心往
2 边扩散;假设最长回文串是奇数,那么以正中心的字符往 2 边扩散。扩散的过程就是对称判断两边字符是否相等的过程。这个方法时间复杂度和动态规划是一样的,但是空间复杂度降低了。时间复杂度
O(n^2),空间复杂度 O(1)。
-
解法三,滑动窗口。这个写法其实就是中心扩散法变了一个写法。中心扩散是依次枚举每一个轴心。滑动窗口的方法稍微优化了一点,有些轴心两边字符不相等,下次就不会枚举这些不可能形成回文子串的轴心了。不过这点优化并没有优化时间复杂度,时间复杂度
O(n^2),空间复杂度 O(1)。 -
解法四,马拉车算法。这个算法是本题的最优解,也是最复杂的解法。时间复杂度 O(n),空间复杂度 O(n)。中心扩散法有 2
处有重复判断,第一处是每次都往两边扩散,不同中心扩散多次,实际上有很多重复判断的字符,能否不重复判断?第二处,中心能否跳跃选择,不是每次都枚举,是否可以利用前一次的信息,跳跃选择下一次的中心?马拉车算法针对重复判断的问题做了优化,增加了一个辅助数组,将时间复杂度从
O(n^2) 优化到了 O(n),空间换了时间,空间复杂度增加到 O(n)。 -
首先是预处理,向字符串的头尾以及每两个字符中间添加一个特殊字符
#
,比如字符串aaba
处理后会变成#a#a#b#a#
。那么原先长度为偶数的回文字符串aa
会变成长度为奇数的回文字符串#a#a#
,而长度为奇数的回文字符串aba
会变成长度仍然为奇数的回文字符串#a#b#a#
,经过预处理以后,都会变成长度为奇数的字符串。**
注意这里的特殊字符不需要是没有出现过的字母,也可以使用任何一个字符来作为这个特殊字符。**
这是因为,当我们只考虑长度为奇数的回文字符串时,每次我们比较的两个字符奇偶性一定是相同的,所以原来字符串中的字符不会与插入的特殊字符互相比较,不会因此产生问题。**
预处理以后,以某个中心扩散的步数和实际字符串长度是相等的。**因为半径里面包含了插入的特殊字符,又由于左右对称的性质,所以扩散半径就等于原来回文子串的长度。 -
核心部分是如何通过左边已经扫描过的数据推出右边下一次要扩散的中心。这里定义下一次要扩散的中心下标是
i
。如果i
比maxRight
要小,只能继续中心扩散。如果i
比maxRight
大,这是又分为 3 种情况。三种情况见上图。将上述 3
种情况总结起来,就是 :dp[i] = min(maxRight-i, dp[2*center-i])
,其中,mirror
相对于center
是和i
中心对称的,所以它的下标可以计算出来是2*center-i
。更新完dp[i]
以后,就要进行中心扩散了。中心扩散以后动态维护最长回文串并相应的更新center
,maxRight
,并且记录下原始字符串的起始位置begin
和maxLen
。
1. Manacher 算法 (基于动态规划)
Manacher 算法是解决最长回文子串问题的高效算法,时间复杂度为 O(n),空间复杂度为 O(n)。它利用了回文串的对称性质,采用了中心扩展法的思想,但是通过一些技巧避免了重复计算,从而大大提高了效率。要理解 Manacher 算法,需要掌握以下基础知识:
- 回文串的性质:理解什么是回文串以及回文串的对称性质是理解 Manacher 算法的基础。
- 中心扩展法:了解如何以某个字符或两个字符为中心,向两边扩展判断回文串。
- 动态规划:理解动态规划的思想和应用场景,Manacher 算法的动态规划部分是核心。
- 数组和字符串操作:熟悉数组和字符串的基本操作,例如索引访问、切片等。
2. 滑动窗口方法
滑动窗口方法是一种直观但效率较低的解决方案,时间复杂度为 O(n^2),空间复杂度为 O(1)。该方法通过遍历所有可能的中心点,以中心点为起始,不断扩展窗口并判断回文串。要理解滑动窗口方法,需要掌握以下基础知识:
- 回文串的定义:了解什么是回文串,即正着读和倒着读都一样的字符串。
- 字符串的基本操作:熟悉字符串的索引访问、切片等基本操作。
- 嵌套循环:理解循环嵌套的概念,以及嵌套循环的时间复杂度。
- 简单的算法思想:了解暴力枚举、遍历等基本的算法思想。
3. 中心扩展法 (两种变种)
中心扩展法也是一种直观但效率较低的解决方案,时间复杂度为 O(n^2),空间复杂度为 O(1)。这个方法的思想是以每个字符或两个字符为中心,向两边扩展判断回文串。要理解中心扩展法,需要掌握以下基础知识:
- 回文串的性质:了解回文串的定义和性质,包括对称性质。
- 字符串的基本操作:熟悉字符串的索引访问、切片等基本操作。
- 嵌套循环:理解循环嵌套的概念,以及嵌套循环的时间复杂度。
- 简单的算法思想:了解暴力枚举、遍历等基本的算法思想。
4. 动态规划法 (DP)
动态规划法是一种通用的解决方案,但在解决最长回文子串问题上效率较低,时间复杂度为 O(n^2),空间复杂度为 O(n^2)。这个方法通过构建一个二维数组来记录子问题的解,然后逐步构建更大的回文串。要理解动态规划法,需要掌握以下基础知识:
- 动态规划的基本思想:了解动态规划的核心思想,即将问题拆分为子问题并记忆子问题的解。
- 二维数组的操作:熟悉二维数组的创建、访问、更新等操作。
- 嵌套循环:理解循环嵌套的概念,以及嵌套循环的时间复杂度。
代码
Go
// 解法一 Manacher's algorithm,时间复杂度 O(n),空间复杂度 O(n)
func longestPalindrome(s string) string {
if len(s) < 2 {
return s
}
newS := make([]rune, 0)
newS = append(newS, '#')
for _, c := range s {
newS = append(newS, c)
newS = append(newS, '#')
}
// dp[i]: 以预处理字符串下标 i 为中心的回文半径(奇数长度时不包括中心)
// maxRight: 通过中心扩散的方式能够扩散的最右边的下标
// center: 与 maxRight 对应的中心字符的下标
// maxLen: 记录最长回文串的半径
// begin: 记录最长回文串在起始串 s 中的起始下标
dp, maxRight, center, maxLen, begin := make([]int, len(newS)), 0, 0, 1, 0
for i := 0; i < len(newS); i++ {
if i < maxRight {
// 这一行代码是 Manacher 算法的关键所在
dp[i] = min(maxRight-i, dp[2*center-i])
}
// 中心扩散法更新 dp[i]
left, right := i-(1+dp[i]), i+(1+dp[i])
for left >= 0 && right < len(newS) && newS[left] == newS[right] {
dp[i]++
left--
right++
}
// 更新 maxRight,它是遍历过的 i 的 i + dp[i] 的最大者
if i+dp[i] > maxRight {
maxRight = i + dp[i]
center = i
}
// 记录最长回文子串的长度和相应它在原始字符串中的起点
if dp[i] > maxLen {
maxLen = dp[i]
begin = (i - maxLen) / 2 // 这里要除以 2 因为有我们插入的辅助字符 #
}
}
return s[begin : begin+maxLen]
}
func min(x, y int) int {
if x < y {
return x
}
return y
}
// 解法二 滑动窗口,时间复杂度 O(n^2),空间复杂度 O(1)
func longestPalindrome(s string) string {
if len(s) == 0 {
return ""
}
left, right, pl, pr := 0, -1, 0, 0
for left < len(s) {
// 移动到相同字母的最右边(如果有相同字母)
for right+1 < len(s) && s[left] == s[right+1] {
right++
}
// 找到回文的边界
for left-1 >= 0 && right+1 < len(s) && s[left-1] == s[right+1] {
left--
right++
}
if right-left > pr-pl {
pl, pr = left, right
}
// 重置到下一次寻找回文的中心
left = (left+right)/2 + 1
right = left
}
return s[pl : pr+1]
}
// 解法三 中心扩散法,时间复杂度 O(n^2),空间复杂度 O(1)
func longestPalindrome(s string) string {
res := ""
for i := 0; i < len(s); i++ {
res = maxPalindrome(s, i, i, res)
res = maxPalindrome(s, i, i+1, res)
}
return res
}
func maxPalindrome(s string, i, j int, res string) string {
sub := ""
for i >= 0 && j < len(s) && s[i] == s[j] {
sub = s[i : j+1]
i--
j++
}
if len(res) < len(sub) {
return sub
}
return res
}
// 解法四 DP,时间复杂度 O(n^2),空间复杂度 O(n^2)
func longestPalindrome(s string) string {
res, dp := "", make([][]bool, len(s))
for i := 0; i < len(s); i++ {
dp[i] = make([]bool, len(s))
}
for i := len(s) - 1; i >= 0; i-- {
for j := i; j < len(s); j++ {
dp[i][j] = (s[i] == s[j]) && ((j-i < 3) || dp[i+1][j-1])
if dp[i][j] && (res == "" || j-i+1 > len(res)) {
res = s[i : j+1]
}
}
}
return res
}
Python
class Solution:
def longestPalindrome(self, s: str) -> str:
if len(s) < 2:
return s
new_s = ['#']
for c in s:
new_s.append(c)
new_s.append('#')
dp = [0] * len(new_s)
max_right = 0
center = 0
max_len = 1
begin = 0
for i in range(len(new_s)):
if i < max_right:
dp[i] = min(max_right - i, dp[2 * center - i])
left = i - (1 + dp[i])
right = i + (1 + dp[i])
while left >= 0 and right < len(new_s) and new_s[left] == new_s[right]:
dp[i] += 1
left -= 1
right += 1
if i + dp[i] > max_right:
max_right = i + dp[i]
center = i
if dp[i] > max_len:
max_len = dp[i]
begin = (i - max_len) // 2
return s[begin:begin + max_len]
class Solution:
# 解法二: 滑动窗口法
def longestPalindrome(self, s: str) -> str:
if len(s) == 0:
return ""
left, right, pl, pr = 0, -1, 0, 0
# 1. 遍历每个字符作为回文中心
while left < len(s):
# 2. 找到相同字符的最右边界
while right + 1 < len(s) and s[left] == s[right + 1]:
right += 1
# 3. 扩展回文边界
while left - 1 >= 0 and right + 1 < len(s) and s[left - 1] == s[right + 1]:
left -= 1
right += 1
# 4. 更新最长回文子串的边界
if right - left > pr - pl:
pl, pr = left, right
# 5. 重置中心,准备下一次寻找
left = (left + right) // 2 + 1
right = left
# 6. 返回结果
return s[pl: pr + 1]
class Solution:
# 解法三: 中心扩散法
def longestPalindrome(self, s: str) -> str:
res = ""
# 1. 以每个字符为中心,寻找最长回文子串
for i in range(len(s)):
res = self.maxPalindrome(s, i, i, res) # 处理奇数长度的回文
res = self.maxPalindrome(s, i, i + 1, res) # 处理偶数长度的回文
# 2. 返回结果
return res
# 辅助函数:在指定范围内寻找最长回文子串
def maxPalindrome(self, s: str, i: int, j: int, res: str) -> str:
sub = ""
while i >= 0 and j < len(s) and s[i] == s[j]:
sub = s[i: j + 1]
i -= 1
j += 1
if len(res) < len(sub):
return sub
return res
class Solution:
# 解法四: 动态规划法
def longestPalindrome(self, s: str) -> str:
res = ""
dp = [[False] * len(s) for _ in range(len(s))]
# 1. 从后往前遍历,填充动态规划表格
for i in range(len(s) - 1, -1, -1):
for j in range(i, len(s)):
dp[i][j] = (s[i] == s[j]) and ((j - i < 3) or dp[i + 1][j - 1])
if dp[i][j] and (not res or j - i + 1 > len(res)):
res = s[i: j + 1]
# 2. 返回结果
return res
Java
class Solution {
public String longestPalindrome(String s) {
if (s.length() < 2) {
return s;
}
// 解法一: Manacher's 算法
StringBuilder newS = new StringBuilder("#");
for (char c : s.toCharArray()) {
newS.append(c);
newS.append('#');
}
// 初始化变量
int[] dp = new int[newS.length()];
int maxRight = 0, center = 0, maxLen = 1, begin = 0;
// Manacher 算法的核心部分
for (int i = 0; i < newS.length(); i++) {
if (i < maxRight) {
dp[i] = Math.min(maxRight - i, dp[2 * center - i]);
}
int left = i - (1 + dp[i]);
int right = i + (1 + dp[i]);
while (left >= 0 && right < newS.length() && newS.charAt(left) == newS.charAt(right)) {
dp[i]++;
left--;
right++;
}
if (i + dp[i] > maxRight) {
maxRight = i + dp[i];
center = i;
}
if (dp[i] > maxLen) {
maxLen = dp[i];
begin = (i - maxLen) / 2;
}
}
// 返回结果
return s.substring(begin, begin + maxLen);
}
}
class Solution {
// 解法二: 滑动窗口法
public String longestPalindrome(String s) {
if (s.length() == 0) {
return "";
}
int left = 0, right = -1, pl = 0, pr = 0;
// 遍历每个字符作为回文中心
while (left < s.length()) {
// 找到相同字符的最右边界
while (right + 1 < s.length() && s.charAt(left) == s.charAt(right + 1)) {
right++;
}
// 扩展回文边界
while (left - 1 >= 0 && right + 1 < s.length() && s.charAt(left - 1) == s.charAt(right + 1)) {
left--;
right++;
}
// 更新最长回文子串的边界
if (right - left > pr - pl) {
pl = left;
pr = right;
}
// 重置中心,准备下一次寻找
left = (left + right) / 2 + 1;
right = left;
}
// 返回结果
return s.substring(pl, pr + 1);
}
}
class Solution {
// 解法三: 中心扩散法
public String longestPalindrome(String s) {
String res = "";
// 以每个字符为中心,寻找最长回文子串
for (int i = 0; i < s.length(); i++) {
res = maxPalindrome(s, i, i, res); // 处理奇数长度的回文
res = maxPalindrome(s, i, i + 1, res); // 处理偶数长度的回文
}
// 返回结果
return res;
}
// 辅助函数:在指定范围内寻找最长回文子串
private String maxPalindrome(String s, int i, int j, String res) {
String sub = "";
while (i >= 0 && j < s.length() && s.charAt(i) == s.charAt(j)) {
sub = s.substring(i, j + 1);
i--;
j++;
}
if (sub.length() > res.length()) {
return sub;
}
return res;
}
}
class Solution {
// 解法四: 动态规划法
public String longestPalindrome(String s) {
String res = "";
boolean[][] dp = new boolean[s.length()][s.length()];
// 从后往前遍历,填充动态规划表格
for (int i = s.length() - 1; i >= 0; i--) {
for (int j = i; j < s.length(); j++) {
dp[i][j] = (s.charAt(i) == s.charAt(j)) && ((j - i < 3) || dp[i + 1][j - 1]);
if (dp[i][j] && (res.isEmpty() || j - i + 1 > res.length())) {
res = s.substring(i, j + 1);
}
}
}
// 返回结果
return res;
}
}
Cpp
class Solution {
public:
string longestPalindrome(string s) {
if (s.length() < 2) {
return s;
}
// 解法一: Manacher's 算法
string newS = "#";
for (char c : s) {
newS += c;
newS += '#';
}
// 初始化变量
vector<int> dp(newS.length(), 0);
int maxRight = 0, center = 0, maxLen = 1, begin = 0;
// Manacher 算法的核心部分
for (int i = 0; i < newS.length(); i++) {
if (i < maxRight) {
dp[i] = min(maxRight - i, dp[2 * center - i]);
}
int left = i - (1 + dp[i]);
int right = i + (1 + dp[i]);
while (left >= 0 && right < newS.length() && newS[left] == newS[right]) {
dp[i]++;
left--;
right++;
}
if (i + dp[i] > maxRight) {
maxRight = i + dp[i];
center = i;
}
if (dp[i] > maxLen) {
maxLen = dp[i];
begin = (i - maxLen) / 2;
}
}
// 返回结果
return s.substr(begin, maxLen);
}
};
class Solution {
public:
// 解法二: 滑动窗口法
string longestPalindrome(string s) {
if (s.length() == 0) {
return "";
}
int left = 0, right = -1, pl = 0, pr = 0;
// 遍历每个字符作为回文中心
while (left < s.length()) {
// 找到相同字符的最右边界
while (right + 1 < s.length() && s[left] == s[right + 1]) {
right++;
}
// 扩展回文边界
while (left - 1 >= 0 && right + 1 < s.length() && s[left - 1] == s[right + 1]) {
left--;
right++;
}
// 更新最长回文子串的边界
if (right - left > pr - pl) {
pl = left;
pr = right;
}
// 重置中心,准备下一次寻找
left = (left + right) / 2 + 1;
right = left;
}
// 返回结果
return s.substr(pl, pr - pl + 1);
}
};
class Solution {
public:
// 解法三: 中心扩散法
string longestPalindrome(string s) {
string res = "";
// 以每个字符为中心,寻找最长回文子串
for (int i = 0; i < s.length(); i++) {
res = maxPalindrome(s, i, i, res); // 处理奇数长度的回文
res = maxPalindrome(s, i, i + 1, res); // 处理偶数长度的回文
}
// 返回结果
return res;
}
// 辅助函数:在指定范围内寻找最长回文子串
string maxPalindrome(string s, int i, int j, string res) {
string sub = "";
while (i >= 0 && j < s.length() && s[i] == s[j]) {
sub = s.substr(i, j - i + 1);
i--;
j++;
}
if (sub.length() > res.length()) {
return sub;
}
return res;
}
};
class Solution {
public:
// 解法四: 动态规划法
string longestPalindrome(string s) {
string res = "";
vector<vector<bool>> dp(s.length(), vector<bool>(s.length(), false));
// 从后往前遍历,填充动态规划表格
for (int i = s.length() - 1; i >= 0; i--) {
for (int j = i; j < s.length(); j++) {
dp[i][j] = (s[i] == s[j]) && ((j - i < 3) || dp[i + 1][j - 1]);
if (dp[i][j] && (res.empty() || j - i + 1 > res.length())) {
res = s.substr(i, j - i + 1);
}
}
}
// 返回结果
return res;
}
};
6. ZigZag Conversion
题目
The string"PAYPALISHIRING"
is written in a zigzag pattern on a given number of rows like this: (you may want to display
this pattern in a fixed font for better legibility)
P A H N
A P L S I I G
Y I R
And then read line by line:"PAHNAPLSIIGYIR"
Write the code that will take a string and make this conversion given a number of rows:
string convert(string s, int numRows);
Example 1:
Input: s = "PAYPALISHIRING", numRows = 3
Output: "PAHNAPLSIIGYIR"
Example 2:
Input: s = "PAYPALISHIRING", numRows = 4
Output: "PINALSIGYAHRPI"
Explanation:
P I N
A L S I G
Y A H R
P I
Example 3:
Input: s = "A", numRows = 1
Output: "A"
Constraints:
1 <= s.length <= 1000
s
consists of English letters (lower-case and upper-case),','
and'.'
.1 <= numRows <= 1000
题目大意
将一个给定字符串 s
根据给定的行数 numRows
,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 "PAYPALISHIRING"
行数为 3 时,排列如下:
P A H N
A P L S I I G
Y I R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"
。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
解题思路
- 这一题没有什么算法思想,考察的是对程序控制的能力。用 2 个变量保存方向,当垂直输出的行数达到了规定的目标行数以后,需要从下往上转折到第一行,循环中控制好方向ji
Go版本:
- 初始化二维字节数组matrix来存储Z字形排列的字符串
- 使用down和up索引控制遍历添加字符的方向
- 当down达到numRows时改变方向,up达到0时也改变方向
- 遍历字符串,根据索引添加到matrix对应的行
- 遍历matrix拼接所有行得到结果
Python版本:
- 初始化字符串列表rows来存储Z字形排列字符串
- 使用going_down标志控制索引变化方向
- 当cur_row到达首尾行时,改变going_down的值
- 遍历字符串,将字符添加到对应的rows行
- 使用join()拼接rows得到结果
Java版本:
- 初始化二维字符数组matrix来存储排列后的字符串
- 使用dir标志控制索引变化方向
- 当curRow到达首尾行时,改变dir方向
- 遍历字符串到matrix对应行
- 遍历matrix拼接每行得到结果
C++版本:
- 初始化字符串向量rows来存储排列后的字符串
- goingDown标志控制索引变化方向
- 当当前行为首尾行时改变goingDown
- 遍历字符串到rows对应的行
- 遍历rows拼接得到结果
代码
package leetcode
func convert(s string, numRows int) string { // 定义函数convert,接受字符串s和整数numRows作为参数,返回字符串
matrix, down, up := make([][]byte, numRows, numRows), 0, numRows-2 // 初始化三个变量:二维字节数组matrix,下标变量down和up
for i := 0; i != len(s); { // 使用for循环遍历字符串s
if down != numRows { // 如果down不等于numRows
matrix[down] = append(matrix[down], byte(s[i])) // 将s的第i个字节加入matrix的第down行
down++ // down自增1
i++ // i自增1
} else if up > 0 { // 否则如果up大于0
matrix[up] = append(matrix[up], byte(s[i])) // 将s的第i个字节加入matrix的第up行
up-- // up自减1
i++ // i自增1
} else { // 否则
up = numRows - 2 // 将up赋值为numRows-2
down = 0 // 将down赋值为0
}
}
solution := make([]byte, 0, len(s)) // 初始化字节数组solution
for _, row := range matrix { // 遍历matrix的每一行
for _, item := range row { // 遍历每一行的每个元素
solution = append(solution, item) // 将该元素加入solution
}
}
return string(solution) // 将solution转换成字符串并返回
}
Python
class Solution:
def convert(self, s: str, numRows: int) -> str:
if numRows == 1: # 如果只有1行,不需转换
return s
rows = ["" for _ in range(numRows)] # 初始化长度为numRows的空字符串列表rows表示矩阵
cur_row = 0 # 当前行编号
going_down = False # 索引变化方向标志
for c in s: # 遍历字符串s的每个字符
rows[cur_row] += c # 在对应行末尾添加字符
if cur_row == 0 or cur_row == numRows - 1: # 如果在第一行或最后一行
going_down = not going_down # 改变索引变化方向
if going_down: # 如果向下遍历
cur_row += 1 # 当前行号+1
else: # 如果向上遍历
cur_row -= 1 # 当前行号-1
return "".join(rows) # 拼接rows得到结果并返回
Java
class Solution {
public String convert(String s, int numRows) {
if (numRows == 1) return s; // 如果只有1行,不需转换
char[][] matrix = new char[numRows][s.length()]; // 初始化二维字符数组matrix,行数为numRows,每行长度为s的长度
int curRow = 0; // 当前行号
int dir = -1; // 索引变化方向,-1代表向上
for (int i = 0; i < s.length(); i++) {
matrix[curRow] = addChar(matrix[curRow], s.charAt(i)); // 将当前字符加入 matrix 的当前行
if (curRow == 0 || curRow == numRows - 1) { // 如果在第一行或最后一行
dir = -dir; // 改变索引变化方向
}
curRow += dir; // 当前行号移动
}
StringBuilder result = new StringBuilder();
for (char[] row : matrix) { // 遍历每一行
for (char c : row) {
if (c != 0) result.append(c); // 添加非空字符到结果字符串
}
}
return result.toString(); // 转成字符串并返回
}
private char[] addChar(char[] array, char c) { // 在数组末尾添加一个字符
if (array == null) {
return new char[] {c};
}
int n = array.length;
char[] newArray = Arrays.copyOf(array, n + 1);
newArray[n] = c;
return newArray;
}
}
Cpp
class Solution {
public:
string convert(string s, int numRows) {
if(numRows == 1) return s; // 只有1行,不需转换
vector<string> rows(numRows); // 初始化长度为numRows的字符串向量rows表示矩阵
int curRow = 0; // 当前行索引
bool goingDown = false; // 索引变化方向标志
for(char c : s) { // 遍历字符串s的每个字符
rows[curRow] += c; // 将字符添加到对应行
if(curRow == 0 || curRow == numRows-1) { // 当前行为第一行或最后一行
goingDown = !goingDown; // 改变索引变化方向
}
if(goingDown) { // 如果向下遍历
curRow++; // 当前行索引+1
} else { // 如果向上遍历
curRow--; // 当前行索引-1
}
}
string ans;
for(string row : rows) { // 遍历每一行
ans += row; // 将行内容拼接到结果字符串
}
return ans; // 返回转换结果
}
};
Go版本:
- make初始化一个切片或映射,可以指定容量
- 二维字节数组matrix存储字符,append向切片追加元素
- range遍历切片或映射
- string()将字节切片转换成字符串
Python版本:
- []创建列表并可以指定长度,+=向列表追加元素
- join()可以拼接列表中的字符串元素
- 不需要预先指定列表长度,根据需求动态增长
Java版本:
- 二维字符数组matrix存储字符,需指定行数和列数
- Arrays.copyOf扩容数组,返回新数组
- StringBuilder用于字符串拼接,toString()转换为String
- charAt()获取字符串指定位置的字符
C++版本:
- vector初始化时可以指定大小
- +=向向量追加元素
- 遍历使用范围for语句,自动获取元素
- string类支持加法拼接
7. Reverse Integer
题目
Given a 32-bit signed integer, reverse digits of an integer.
Example 1:
Input: 123
Output: 321
Example 2:
Input: -123
Output: -321
Example 3:
Input: 120
Output: 21
**Note:**Assume we are dealing with an environment which could only store integers within the 32-bit signed integer range: [−2^31, 2^31 − 1]. For the purpose of this problem, assume that your function returns 0 when the reversed integer overflows.
题目大意
给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。注意:假设我们的环境只能存储得下 32 位的有符号整数,则其数值范围为 [−2^31, 2^31 − 1]。请根据这个假设,如果反转后整数溢出那么就返回 0。
解题思路
- 这一题是简单题,要求反转 10 进制数。类似的题目有第 190 题。
- 这一题只需要注意一点,反转以后的数字要求在 [−2^31, 2^31 − 1]范围内,超过这个范围的数字都要输出 0 。
Go 版本
- 定义变量 tmp 来保存反转后的结果,初始化为 0
- 使用 for 循环,当 x 不等于 0 时执行循环
- 在循环内,通过 x 对 10 求余(x%10)获取末位数字,并将其乘以 10 加到 tmp 后面
- 通过 x 整除 10(x/10)去掉 x 的末位数字
- 循环直至 x 为 0
- 判断 tmp 是否超出 int32 范围,如果是返回 0
- 最后返回 tmp 作为结果
Python 版本
- 定义 result 变量,初始化为 0
- 判断 x 是否为负数,是则将符号设为 -1,并将 x 转正
- 使用 while 循环,当 x 不等于 0 时执行循环
- 在循环内,获取 x 对 10 求余的末位数字,累加到 result 末尾
- 将 x 整除 10 删除末位数字
- 循环直至 x 为 0
- 根据符号将 result 还原为负数
- 判断 result 是否越界,是则返回 0
- 返回 result
Java 版本
- 定义 result 变量,初始化为 0
- 判断 x 是否为负数,是则将 x 转为正数
- 使用 while 循环,当 x 不等于 0 时循环
- 在循环内,获取 x 对 10 求余的末位数字,累加到 result 末尾
- 将 x 整除 10 删除末位数字
- 循环直至 x 为 0
- 在循环中判断是否越界,是则返回 0
- 如果为负数,将 result 变为负数
- 返回 result
C++版本
- 定义 result 变量,初始化为 0
- 判断 x 是否为负数,是则将 x 转为正数
- 检查是否为最小值,是则直接返回 0
- 使用 while 循环,当 x 不等于 0 时循环
- 在循环内,获取 x 对 10 求余的末位数字,累加到 result 末尾
- 将 x 整除 10 删除末位数字
- 在循环中判断是否越界,是则返回 0
- 如果为负数,将 result 变为负数
- 返回 result
Go
func reverse(x int) int {
tmp := 0 // 定义一个临时变量tmp,初始化为0
for x != 0 { // 当x不等于0时,进行循环
tmp = tmp*10 + x%10 // 将x对10取余,加到tmp末尾,实现逆序
x = x / 10 // x整除10,去掉末尾数字
}
if tmp > 1<<31-1 || tmp < -(1<<31) { // 检查tmp是否超出int范围
return 0 // 如果超出范围,返回0
}
return tmp // 返回tmp
}
Python
class Solution:
def reverse(self, x: int) -> int:
result = 0 # 结果初始化为0
if x < 0: # 判断x是否为负数
symbol = -1 # 如果x为负数,符号设为-1
x = -x # 将x变正
else:
symbol = 1 # 否则符号为1
while x != 0:
result = result * 10 + x % 10 # 取x的末尾数字,加到result末尾
x = x // 10 # x去掉末尾数字
result = result * symbol # 恢复result的符号
if result >= 2**31 or result < -2**31: # 判断是否越界
return 0
else:
return result
Java
class Solution {
public int reverse(int x) {
int result = 0; // 结果初始化为0
boolean isNegative = x < 0; // 判断x是否为负数
if(isNegative) {
x = -x; // 如果为负数,将x变为正数
}
while(x != 0) {
int pop = x % 10; // x对10取余,获取末位数字
x /= 10; // x除以10,删除末位数字
if (result > Integer.MAX_VALUE/10 || (result == Integer.MAX_VALUE / 10 && pop > 7)) {
return 0; // 检查是否越上界
}
if (result < Integer.MIN_VALUE/10 || (result == Integer.MIN_VALUE / 10 && pop < -8)) {
return 0; // 检查是否越下界
}
result = result * 10 + pop; // 结果乘10后加上末位数字
}
if(isNegative) {
result = -result; // 如果为负数,结果取反
}
return result;
}
}
Cpp
class Solution {
public:
int reverse(int x) {
int result = 0; // 结果初始化为0
bool isNegative = x < 0; // 判断是否为负数
if (isNegative) {
if (x == INT_MIN) { // 如果是最小值,取反会溢出,直接返回0
return 0;
}
x = -x; // 负数取反使之为正数
}
while (x != 0) {
int pop = x % 10; // 取余取得末尾数字
x /= 10; // 移除末尾数字
if (result > INT_MAX/10 || (result == INT_MAX/10 && pop > INT_MAX%10)) {
return 0; // 检查越界
}
if (result < INT_MIN/10 || (result == INT_MIN/10 && pop < INT_MIN%10)) {
return 0;
}
result = result * 10 + pop; // 组装结果
}
if (isNegative) {
result = -result; // 恢复负号
}
return result;
}
};
Go语言基础知识:
- var 定义变量,可以初始化值
- for 循环
- if 条件判断
- % 取余运算
- / 除法运算
- 右移运算符 >> ,可以用于快速计算2的指数
- 返回值通过 return 关键字返回
Python基础知识:
- result = 0 定义变量并初始化
- if/else 条件判断
- while 循环
- % 取余
- // 整数除法
- ** 幂运算符,可以计算2的指数
- return 直接返回值
Java基础知识:
- int result = 0 定义变量并初始化
- if/else 条件判断
- while 循环
- % 取余
- / 除法
- Math.pow(2, n) 计算2的指数
- return 返回值
C++基础知识:
- int result = 0 定义变量并初始化
- bool 类型判断真假
- if/else 条件判断
- while 循环
- % 取余运算
- / 除法
- pow(2, n) 计算2的指数
- return 返回值
此外,Java和C++版本要注意反转最小值时的溢出问题,需要特判。