原题链接:力扣热题-HOT100
题解的顺序和题目的顺序一致,那么接下来就开始刷题之旅吧
1.两数之和
思路1:暴力枚举法
暴力枚举法很简单,遍历nums
数组的每一个元素,找到和target-nums[i]
相同的元素即可。
时间复杂度:需要使用到两层for
循环,所以时间复杂度为O(n^2)
。
代码实现:
class Solution {
public int[] twoSum(int[] nums, int target) {
for(int i=0;i<nums.length;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[j]==target-nums[i]){
return new int[] {i,j};
}
}
}
return null;
}
}
思路2:哈希表法
使用哈希表来解决该问题的思路是,创建一个哈希表来存放数据,其中key
为target-nums[i]
,value
为对应的索引。在遍历数组时,将数组中元素的值和对应的索引记录到哈希表中,检查当前元素的补数(target-nums[i]
)是否在哈希表中,如果在,其value
即为所求,若不在,将元素记录到哈希表中并接着遍历数组。
时间复杂度:只需要遍历一次数组,用到一个for
循环,所以时间复杂度为O(n)
。
代码实现:
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> tab = new HashMap<>();
for(int i=0;i<nums.length;i++){
int tar = target-nums[i];
if(tab.containsKey(tar)){
return new int[] {i,tab.get(tar)};
}else{
tab.put(nums[i],i);
}
}
return null;
}
}
2.两数相加
思路:
使用初等数学的直接相加即可,主要考察对链表的操作,在相加的过程中如果链表的长度不一样,在较短的链表后面补0即可,使用carry
来记录进位,carry
不为0时表示需要进行进位操作,carry
的值使用sum/10
,创建新结点的值使用sum%10
。
注:详细的解题思路参见图解-两数相加
时间复杂度:
该思路的时间复杂度为O(n+m)
,空间复杂度也为O(n+m)
,其中n和m分别为两链表的长度。
代码实现:
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = new ListNode(-1);
ListNode tail = head;
int sum;
int carry = 0;
while(l1 != null || l2 != null){
int n = l1 == null ? 0 : l1.val;
int m = l2 == null ? 0 : l2.val;
sum = n + m + carry;
carry = sum / 10;
tail.next = new ListNode(sum % 10);
tail = tail.next;
l1 = l1 != null ? l1.next : null;
l2 = l2 != null ? l2.next : null;
}
if(carry != 0){
tail.next = new ListNode(carry);
}
return head.next;
}
};
3.无重复字符的最长子串
思路:
该题考察的是滑动窗口的相关知识,首先我们要考虑字符串为空的情况,这是最容易忽略的一个点,用一个简单的判断语句即可解决。排除了这种情况之后我们创建一个哈希表,初始化左指针left
和max
的值为0,使用for循环来遍历整个字符串,首先,判断当前字符是否在map
中,如果不在,则将该字符添加到map(字符,字符索引)
,此时没有出现重复的字符,所以left
不需要变化,无重复子串的最大长度为i-left+1
,与原来max
比较取最大值。如果map
中包含字符,则会出现两种情况:
- 当前字符包含在有效子段中,那么此时需要更新
left
的值(left=map.get(s.charAt(i))+1
) - 当前字符不在有效子段中,我们发现此时,更新
left
的结果同第一种情况
综上,我们使用left=Math.max(left,map.get(s.charAt(i))+1)
更新left
的值。对于max的值每次都与i-left+1
作比较,取最大值。
注:如果对上面的思路不是很理解,可以参考详解思路-无重复字符的最长子串
时间复杂度:
使用了一层for
循环,所以时间复杂度为O(n)
代码实现:
class Solution {
public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> map =new HashMap<Character,Integer>();
if(s.length()==0){
return 0;
}
int max=0;
int left=0;
for(int i = 0;i<s.length();i++){
if(map.containsKey(s.charAt(i))){
left = Math.max(left,map.get(s.charAt(i))+1);
}
map.put(s.charAt(i),i);
max=Math.max(max,i-left+1);
}
return max;
}
}
4.寻找两个正序数组的中位数
思路:
该题的解决采用二分思想,只需要给出两个有序数组一个恰当的【分割线】,中位数的值就由位于这个【分割线】的两侧的数来决定,确定分割线的位置使用二分查找法,需要注意的点是:分割线左边的所有元素的数值<分割线右边所有元素的数值。
注:这里的思路写的比较简单,详细的可以看这个视频详细思路-寻找两个正序数组的中位数
时间复杂度:
时间复杂度为O(log min(m,n)),m,n分别为两数组的长度,空间复杂度为O(1)
代码实现:
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
//将nums1设置为长度较小的数组,方便代码的编写
if(nums1.length>nums2.length){
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length;
int n = nums2.length;
//计算分割线左边元素的数量
int totalleft = (m+n+1)/2;
//二分法查找nums1部分的分割线
int left = 0;
int right = m;
while(left<right){
//这是对于(left+right)/2的特殊处理方式,防止发生整型溢出
//同时使用二分法时,如果出现left=i,则这里需要+1,否则不需要
int i = left+(right-left+1)/2;
int j = totalleft - i;
if(nums1[i-1] > nums2[j]){
right = i - 1;
}else{
left = i;
}
}
int i = left;
int j = totalleft - i;
//最后得到两个数组分割线左右两边元素的最大值的最小值
//为了防止出现分割线左右两边没有元素的极端情况加上判断
int nums1LeftMax = i ==0 ? Integer.MIN_VALUE : nums1[i-1];
int nums1RightMin = i ==m ? Integer.MAX_VALUE : nums1[i];
int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j-1];
int nums2RightMin = j == n ? Integer.MAX_VALUE : nums2[j];
//计算中位数的值
if(((m+n) % 2) == 1){
return Math.max(nums1LeftMax,nums2LeftMax);
}else{
return (double) ((Math.max(nums1LeftMax,nums2LeftMax) + Math.min(nums1RightMin,nums2RightMin))) / 2;
}
}
}
5.最长回文子串
本题官方共给出了四种解决方法,分别为暴力解法,中心位置法,动态规划法和Manacher方法,在这里主要介绍动态规划法这种最常用的方法,对于暴力解法,只给出判断回文子串的核心代码。对其他两种解法感兴趣的可以点击四种解法-最长回文串观看详细视频。
思路1:
暴力解法遍历字符串即可,其中判断回文子串的核心代码如下:
private boolean validPalindromic(char[] charArry,int left,int right){
while(left<right){
if(charArry[left] != charArry[right]){
return false;
}
left++;
right--;
}
return true;
}
思路2:
动态规划法:动态规划法的关键在与确定状态转移方程,在本题中,如果一个字符串的最左边和最右边的字符相同,那么说明该字符串是否为回文串取决于其中间部分的子串,我们可以继续比较中间子串的最左边和最右边字符是否相同,依此类推…。我们使用一个二维数组dp[i][j]
来表示子串s[i...j]
是否为回文子串,那么状态转移方程为dp[i][j]=(s[i])==s[j]) && dp[i+1][j-1]
。边界条件为j-i<3
(当j-i
为1或者2的时候字符串中间分别为0或者1个元素,不需要进行判断,如ab,aba),初始化数组dp
,由于对角线位置s[i]==s[j]
,因此对角线位置全为true
,我们记录下最长回文串的长度和初始位置,即可进行回文串的截取。
时间复杂度:
该解法的时间复杂度为O(n^2)
(n为字符串长度),空间复杂度为O(n^2)
代码实现:
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if(len<2){
return s;
}
int maxLen = 1;
int begin = 0;
//初始化二维数组
boolean[][] dp = new boolean[len][len];
for(int i=0;i<len;i++){
dp[i][i] = true;
}
char[] charArray = s.toCharArray();
for(int j=1;j<len;j++){
for(int i=0;i<j;i++){
if(charArray[i] != charArray[j]){
dp[i][j]=false;
}else{
if(j-i<3){
dp[i][j]=true;
}else{
dp[i][j] = dp[i+1][j-1];
}
}
if(dp[i][j] && j-i+1 > maxLen){
maxLen = j-i+1;
begin = i;
}
}
}
return s.substring(begin,begin+maxLen);
}
}
6.Z字形变换
思路:
该题的解题思路是首先创建一个空数组,长度为numRows
,然后遍历字符串,向数组中放入字符,首先放入的方向是朝下的,如图,A放在0,B放在1,C放在2,然后需要转换方向,设置一个参数down
用于表示方向的转换,转换方向的条件是前面设置的数组的下表row==0
或者row==numRows-1
,转换后更新一下row
的值,向下时row
每次+1,向上时row
每次-1,这样就把每个字符拼接到数组中,最后将遍历到string
类型的结果值中并返回即可。如果上述思路没有很明白,可以点击视频详解-Z字形变换学习。
时间复杂度:
使用到一层for循环,时间复杂度为O(n^2)
代码实现:
class Solution {
public String convert(String s, int numRows) {
if (numRows == 1) {
return s;
}
ArrayList<String> rows = new ArrayList<>(numRows);
for (int i = 0; i < numRows; i++) {
rows.add("");
}
boolean down = false;
for (int i = 0, row = 0; i < s.length(); i++) {
rows.set(row, rows.get(row) + s.charAt(i));
if (row == 0 || row == numRows - 1)
down = !down;
row += down ? 1 : -1;
}
String ans = "";
for (int i = 0; i < numRows; i++)
ans += rows.get(i);
return ans;
}
}
一些知识点:
这里使用到了ArrayList
,其常用的函数参考ArrayList常见方法
7.整数反转
思路:
该题的解题思路是将给定的数字依次取出其各位数字,并依次推入新的数字中,其中取出个位数字使用取余操作,比较重要的一点是对溢出的判断,判断条件如下图右边部分推出,详细的可以参考视频讲解-整数反转。
时间复杂度:
时间复杂度为O(n)
,n
为x
的长度。
代码实现:
class Solution {
public int reverse(int x) {
int ans = 0;
while(x!=0){
if(x > 0 && ans > (Integer.MAX_VALUE - x % 10) / 10) return 0;
if(x < 0 && ans < (Integer.MIN_VALUE - x % 10) / 10) return 0;
ans = ans * 10 + x % 10;
x /= 10;
}
return ans;
}
}
8.字符串转换整数(atoi)
思路:
首先去掉空格,然后设置sign来确定正负号,如果有“-”,则sign=-1
,反之为1,去掉空格并确定正负号后将字符串中的连续数字加入结果中,表达式为ans = ans * 10 + sign * (charArry[i]-'0')
,最后需要进行溢出判断,如果大于最大值或者小于最小值,则直接输出最大值和最小值。详细的视频讲解点击视频讲解-字符串转换整数。
时间复杂度:
时间复杂度为O(n)
。
代码实现:
class Solution {
public int myAtoi(String s) {
int ans = 0;
//全是空格
int start = 0;
char[] charArry = s.toCharArray();
while(start<s.length() && charArry[start] == ' ') start++;
if(start == s.length()) return 0;
//提取正负号
int sign = 1;
if(charArry[start] == '-'){
sign = -1;
start++;
}else if(charArry[start] == '+') start++;
for(int i=start;i<s.length() && Character.isDigit(charArry[i]);i++){
ans = ans * 10 + sign * (charArry[i]-'0');
if(ans > 0 && ans >= Integer.MAX_VALUE / 10) return Integer.MAX_VALUE;
if(ans < 0 && ans <= Integer.MIN_VALUE / 10) return Integer.MIN_VALUE;
}
return ans;
}
}
注:溢出判断存在问题,有一些测试用例没有通过,目前还没找到解决办法,如果有小伙伴有好的方法,欢迎留言讨论。
更新:来更新啦,第8题有一部分测试用例没有通过,本来我以为是边界判断出错了,但其实是数据类型的问题,之前在定义ans
是使用的是int
数据类型,后来为了溢出换成了long
类型,但是发现运行会报错,所以就又改回int
,这也是部分用例未通过的原因,正确的做法是将ans
定义成为long
类型,报错的原因是long
类型的ans
不能作为int
类型返回,必须要强转为int
类型。即return
那要改为return (int)ans==ans?(int)ans:0;
更新代码:
class Solution {
public int myAtoi(String s) {
long ans = 0;
//全是空格
int start = 0;
char[] charArry = s.toCharArray();
while(start<s.length() && charArry[start] == ' ') start++;
if(start == s.length()) return 0;
//提取正负号
int sign = 1;
if(charArry[start] == '-'){
sign = -1;
start++;
}else if(charArry[start] == '+') start++;
for(int i=start;i<s.length() && Character.isDigit(charArry[i]);i++){
ans = ans * 10 + sign * (charArry[i]-'0');
if(ans > 0 && ans >= Integer.MAX_VALUE) return Integer.MAX_VALUE;
if(ans < 0 && ans <= Integer.MIN_VALUE) return Integer.MIN_VALUE;
}
return (int)ans==ans?(int)ans:0;
}
}
注:需要补充的是,如果对于每题的思路不是很理解,可以点击链接查看视频讲解,是我在B站发现的一个宝藏UP主,视频讲解很清晰(UP主用的是C++),可以结合视频参考本文的java代码。
待续…