第一题:两数相加
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。 如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例: 输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
分析:最开始想的是分别将这两个链表转换为int型的整数,然后两数相加得到一个int整数,最后将这个整数转成链表。可惜这个想法太天真了,不仅时间复杂度很高,而且还会出现int溢出的问题。最后看了答案:
public ListNode AddTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(0);
ListNode p = l1,q = l2,curr = dummyHead;
int carry = 0;
while (p != null || q!= null){
int x = (p != null) ? p.val : 0;
int y = (q != null) ? q.val : 0;
int sum = x + y + carry;
carry = sum / 10;
curr.next = new ListNode(sum%10);
curr = curr.next;
if(p != null){
p = p.next;
}
if( q != null){
q = q.next;
}
}
if (carry > 0) {
curr.next = new ListNode(carry);
}
return dummyHead.next;
}
在这里我们使用了一个dummyHead作为头节点之前的节点。0-9之内的数字相加有可能出现溢出的问题使用carry保存溢出的十位上的数字。最后还有可能出现进位的问题,所以要对carry进行判断。
第二题: 寻找两个正序数组的中位数
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
示例 1:
nums1 = [1,3]nums2 = [2]则中位数是2.0
示例 2:
nums1 = [1,2]nums2 = [3,4]则中位数是 (2+3)/2=2.5
分析:刚开始的时候我的思路是将这两个数组合并为一个大数组,然后根据合并后数组的长度计算中位数。但是这种算法的时间复杂度为O(m+n),不能满足题目的要求。利用二分发可以满足复杂度 O(log(m + n))。
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
int leftLength = nums1.length;
int rightLength = nums2.length;
// 为了保证第一个数组比第二个数组小(或者相等)
if (leftLength > rightLength) {
return findMedianSortedArrays(nums2, nums1);
}
while (left < right) {
// 二分查找,此处为取第一个数组中左右指针下标的中位数,决定起始位置
// 此处+1首先是为了不出现死循环,即left永远小于right的情况
// left和right最小差距是1,此时下面的计算结果如果不加1会出现i一直=left的情况,而+1之后i才会=right
// 于是在left=i的时候可以破坏循环条件,其次下标+1还会保证下标不会越界,因为+1之后向上取整,保证了
// i不会取到0值,即i-1不会小于0
// 此时i也代表着在一个数组中左边的元素的个数
int i = left + (right - left + 1) / 2;
// 第一个数组中左边的元素个数确定后,用左边元素的总和-第一个数组中元素的总和=第二个元素中左边的元素的总和
// 此时j就是第二个元素中左边的元素的个数
int j = totalLeft - i;
// 此处用了nums1[i - 1] <= nums2[j]的取反,当第一个数组中分割线的左边的值大于第二个数组中分割线的右边的值
// 说明又指针应该左移,即-1
if (nums1[i - 1] > nums2[j]) {
// 下一轮搜索的区间 [left, i - 1]
right = i - 1;
// 此时说明条件满足,应当将左指针右移到i的位置,至于为什么是右移,请看i的定义
} else {
// 下一轮搜索的区间 [i, right]
left = i;
}
}
// 退出循环时left一定等于right,所以此时等于left和right都可以
// 为什么left一定不会大于right?因为left=i。
// 此时i代表分割线在第一个数组中所在的位置
// 分割线左边的所有元素需要满足的个数 m + (n - m + 1) / 2;
// 两个数组长度之和为偶数时,当在长度之和上+1时,由于整除是向下取整,所以不会改变结果
// 两个数组长度之和为奇数时,按照分割线的左边比右边多一个元素的要求,此时在长度之和上+1,就会被2整除,会在原来的数
//的基础上+1,于是多出来的那个1就是左边比右边多出来的一个元素
int totalLeft = (leftLength + rightLength + 1) / 2;
// 在 nums1 的区间 [0, leftLength] 里查找恰当的分割线,
// 使得 nums1[i - 1] <= nums2[j] && nums2[j - 1] <= nums1[i]
int left = 0;
int right = leftLength;
// nums1[i - 1] <= nums2[j]
// 此处要求第一个数组中分割线的左边的值 不大于(小于等于) 第二个数组中分割线的右边的值
// nums2[j - 1] <= nums1[i]
// 此处要求第二个数组中分割线的左边的值 不大于(小于等于) 第一个数组中分割线的右边的值
// 循环条件结束的条件为指针重合,即分割线已找到
// nums1[i]为第一个数组中分割线右边的第一个值
// nums[i-1]即第一个数组中分割线左边的第一个值
int i = left;
// 此时j代表分割线在第二个数组中的位置
// nums2[j]为第一个数组中分割线右边的第一个值
// nums2[j-1]即第一个数组中分割线左边的第一个值
int j = totalLeft - i;
// 当i=0时,说明第一个数组分割线左边没有值,为了不影响
// nums1[i - 1] <= nums2[j] 和 Math.max(nums1LeftMax, nums2LeftMax)
// 的判断,所以将它设置为int的最小值
int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
// 等i=第一个数组的长度时,说明第一个数组分割线右边没有值,为了不影响
// nums2[j - 1] <= nums1[i] 和 Math.min(nums1RightMin, nums2RightMin)
// 的判断,所以将它设置为int的最大值
int nums1RightMin = i == leftLength ? Integer.MAX_VALUE : nums1[i];
// 当j=0时,说明第二个数组分割线左边没有值,为了不影响
// nums2[j - 1] <= nums1[i] 和 Math.max(nums1LeftMax, nums2LeftMax)
// 的判断,所以将它设置为int的最小值
int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
// 等j=第二个数组的长度时,说明第二个数组分割线右边没有值,为了不影响
// nums1[i - 1] <= nums2[j] 和 Math.min(nums1RightMin, nums2RightMin)
// 的判断,所以将它设置为int的最大值
int nums2RightMin = j == rightLength ? Integer.MAX_VALUE : nums2[j];
// 如果两个数组的长度之和为奇数,直接返回两个数组在分割线左边的最大值即可
if (((leftLength + rightLength) % 2) == 1) {
return Math.max(nums1LeftMax, nums2LeftMax);
} else {
// 如果两个数组的长度之和为偶数,返回的是两个数组在左边的最大值和两个数组在右边的最小值的和的二分之一
// 此处不能被向下取整,所以要强制转换为double类型
return (double) ((Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin))) / 2;
}
}
这种方式基于一种分割线的思路来寻找中位数。第一个数组有一个分割线,第二个数组也有一个分割线,分割线左侧的所有元素要小于分割线右侧的所有元素,也就意味着第一个数组的分割线左侧的最后一个元素要小于第二个数组分割线右侧的第一个元素,同理,第二个数组的分割线左侧的最后一个元素要小于第一个数组分割线右侧的第一个元素。同时,上下两个数组分割线左侧的元素的数量是(上面数组的元素个数+下面数组的元素的个数+1)/2。当总元素的个数是偶数的时候,左侧的元素的数量是总元素数量的一半,当是奇数的时候,左侧元素的数量是总元素的数量的一半再加一。这种思路相当于只要找到上下两个数组的分割线两侧的四个元素就可以计算出中位数了。
需要注意的是有四种特殊情况:上面的数组分割线左边没有元素、右边没有元素,下面的数组分割线左边没有元素,右边没有元素。
第三题: 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入:"babad"输出:"bab"注意:"aba"也是一个有效答案。
示例 2:
输入:"cbbd"输出:"bb"
示例 3:
输入:"a"输出:"a"
解法:动态规划
使用一个二维数组表示各个阶段的状态,这个二维数组的行是子串的起始位置,列是子串的结束位置。由于j>=i,所以只需要考虑二维数组的主对角线的上半部分,对角线上的值永远是true。用true表示这个子串是回文串,false不是回文串。那么对于某个固定位置的数组元素来说,它的值依赖于左下角的元素的值。进行填充的时候只能一列一列地进行填充,同一列的元素从上到下依次填充。
public String longestPalindrome(String s) {
int len = s.length();
// 特判
if (len < 2){
return s;
}
//最大长度初始是1
int maxLen = 1;
int begin = 0;
// 1. 状态定义
// dp[i][j] 表示s[i...j] 是否是回文串
// 2. 初始化
boolean[][] dp = new boolean[len][len];
for (int i = 0; i < len; i++) {
dp[i][i] = true;
}
char[] chars = s.toCharArray();
// 3. 状态转移
// 注意:先填左下角
// 填表规则:先一列一列的填写,再一行一行的填,保证左下方的单元格先进行计算
for (int j = 1;j < len;j++){
for (int i = 0; i < j; i++) {
// 头尾字符不相等,不是回文串
if (chars[i] != chars[j]){
dp[i][j] = false;
}else {
// 相等的情况下
// 考虑头尾去掉以后没有字符剩余,或者剩下一个字符的时候,肯定是回文串
if (j - i < 3){
dp[i][j] = true;
}
//否则,判断其左下角的元素的状态
else {
// 状态转移
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要dp[i][j] == true 成立,表示s[i...j] 是否是回文串
// 此时更新记录回文长度和起始位置
if (dp[i][j] && j - i + 1 > maxLen){
maxLen = j - i + 1;
begin = i;
}
}
}
// 4. 返回值
return s.substring(begin,begin + maxLen);
}
}