寻找两个正序数组的中位数
题目描述:
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。
进阶:你能设计一个时间复杂度为 O(log (m+n)) 的算法解决此问题吗?
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
示例 3:
输入:nums1 = [0,0], nums2 = [0,0]
输出:0.00000
示例 4:
输入:nums1 = [], nums2 = [1]
输出:1.00000
示例 5:
输入:nums1 = [2], nums2 = []
输出:2.00000
解题思路:
首先我们需要明白一点,那就是重新组合后的数组长度为奇数或者偶数的时候,中位数的计算方式是不一样的.
如果组合之后长度是奇数,那么中位数就应该是下图所示:
如果组合之后长度是偶数,那么中位数就应该是下图所示:
所以到最后我们肯定是要分情况讨论的.
其次如果一点都不考虑复杂度的话,我们可以直接将两个正序序列重新组合成一个正序序列,这样我们就可以我们只需要分长度是偶数还是奇数讨论即可.这个就对应我的第一版代码.
如果想要优化一下算法的话,我们其实可以想一想,既然他让我们找出两个序列的的中位数,那么很显然我们只要将两个数组组合到一半就行了,剩下的元素我们就没有必要继续继续组合了,这样 时间复杂度和空间复杂度都只有原来的一半 了.毕竟只遍历的一半的元素.对应我的第二版代码.
源代码:
这是我的第一版代码:
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { List<Integer>list=new ArrayList<Integer>(); int i=0; int j=0; //重新组合序列 while(i<nums1.length&&j<nums2.length) { if(nums1[i]<=nums2[j]) { list.add(nums1[i++]); } else { list.add(nums2[j++]); } } //如果跳出上面的循环,那么显然至少有一个数组已经遍历结束了 //那么很显然我们就只需要再吧最后剩下的数组遍历结束即可 while(j<nums2.length) { list.add(nums2[j++]); } //这里理由同上 while(i<nums1.length) { list.add(nums1[i++]); } //分情况返回或者中位数 if(list.size()%2==0) { double result=(double)(list.get(list.size()/2-1)+list.get(list.size()/2))/2; return result; } else { return list.get(list.size()/2); } } }
这是我的第二版代码:
class Solution { public double findMedianSortedArrays(int[] nums1, int[] nums2) { int sum=nums1.length+nums2.length; //这里我们将数组的长度定义为+1,是为了包容两种情况 //否则又得分情况讨论 int nums3[]=new int [sum/2+1]; int i=0,j=0,k=0; //重新组合序列 while (i<nums3.length) { if(j<nums1.length&&k<nums2.length) { if(nums1[j]<nums2[k]) { nums3[i++]=nums1[j++]; } else { nums3[i++]=nums2[k++]; } } else break; } //这里和第一版的代码差不多,只不过多了一步判断是否越过重新组合序列长度 while(i<nums3.length&&j<nums1.length) { nums3[i++]=nums1[j++]; } while(i<nums3.length&&k<nums2.length) { nums3[i++]=nums2[k++]; } //分情况返回或者中位数 if(sum%2==0) return (double)(nums3[nums3.length-2]+nums3[nums3.length-1])/2; else return nums3[nums3.length-1]; } }
虽然两者的代码看起来没有什么变化,但是大家可能测试比较一下两版的代码,会发现不管是在时间复杂度或者是空间复杂度上都有了明显的提升.
!!!最长回文子串!!!(重点掌握)
题目描述:
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
暴力求解
解题思路:
当然了不动脑子的话,很容易想到暴力求解.只需要从最长的字符串开始递归进行检查,看是不是回文串,如果是的话,那么就直接返回即可.
Up一开始就是不动脑子,直接暴力的,暴力求解思路很简单并且代码编写也简单,并且可读性也比较好.但是缺点也很明显那就是时间复杂度太高.指不定就时间就超了.
源代码:
class Solution { //判断是不是回文串 public boolean panduan(String string1) { char[]ch=string1.toCharArray(); for(int i=0;i<ch.length/2;i++) { if(ch[i]!=ch[ch.length-i-1]) return false; } return true; } //暴力求解 public String longestPalindrome(String s) { for(int i=s.length();i>0;i--) { for(int j=0;j<s.length()-i+1;j++) { if(panduan(s.substring(j, j+i))) { return s.substring(j, j+i); } } } return ""; } }
我们可以看到值通过了不到一般的样例,所以我们必须要使用其他的方法.
动态规划
解题思路:
其实我们看过上面暴力求解的算法之后,我们就能看到,这个过程中 最耗费时间的就是判断该字符串是不是回文串,所以如果我们能够非常快速的就能判断一个字符串是不是回文串的话,那么很显然我们的时间复杂度就能够大幅度的降低了.
这里我们就来分析一下我们怎么能快速的判断一个字符串是不是回文串呢.可以看到这部分我用的是动态规划,所以关于动态规划,大家首先需要考虑的就是找到状态转移方程!!!
这里我们不妨先作出下面这两个假设:
1.假设dp[i][j]代表字符串下标从i到j的的字符串
dp[i+1][j-1]代表字符串下标从i+1到j-1的的字符串
2.假设我们已经知道了dp[i+1][j-1]是不是回文串了之后,我们如何快速的判断dp[i][j]是不是回文串呢?
根据回文串的定义,肯定是要首尾字符对应的,那么很显然我们是不是能够推导出下面状态转移方程:
dp[i][j]为回文串 等价于 dp[i+1][j-1]为回文串&&s[i]=s[j],方程图解如下图所示:
既然我们有了这个转台转移方程之后我们就只剩下一件事了,那就是定义我们的初始状态,根据我们的状态转移方程,我们可以知道,想知道长的字符串是不是回文串就必须要知道短的字符串的状态,所以我们可以推断出,这里的初始状态应该就字符串中每一个单个字符的状态,并且单个字符我们都是看做是回文串的.那么很显然我们的转台转移返程以及初始状态我们都已经找到了.那么接下来我们就可以编写我们的代码了.
源代码:
class Solution { public String longestPalindrome(String s) { boolean [][]dp=new boolean[s.length()][s.length()]; char []schar=s.toCharArray(); int max=Integer.MIN_VALUE; int begin=0; int end=0; for(int i=0;i<schar.length;i++) { dp[i][i]=true; } //k为步长,i为起点,j为终点 for(int k=1;k<schar.length;k++) { for(int i=0;i<schar.length-k;i++) { int j=i+k; //处理回文串长度为偶数的情况 if(j-i==1) { if(schar[i]==schar[j]) { dp[i][j]=true; //记录最大的回文串索引 if(j-i+1>max) { max=j-i+1; begin=i; end=j; } } else dp[i][j]=false; } //防止数组越界 else if(i+1<=j-1) { if(schar[i]==schar[j]&&dp[i+1][j-1]) { dp[i][j]=true; //记录最大的回文串索引 if(j-i+1>max) { max=j-i+1; begin=i; end=j; } } else dp[i][j]=false; } } } return s.substring(begin, end+1); } }
中心扩散法
解题思路:
中心扩散法的思想就是以一个元素为中间点,然后向两边进行扩散,扩散的过程找到以该元素为中间点的最大的回文串.因为只遍历了一次字符串,所以使得时间复杂度达到了线性级别即O(n),相比我们上面的动态规划,会显得更加的快速.
但是这个扩散的过程还有这个注意点,就是那些元素可以当做是中间点呢?
这个就需要我们再次分情况讨论了:
当我们的字符串长度是偶数的时候,那么很显然中间点应该是这样的:
中间点并不是指向一个元素的,而是一个夹缝.
但是当我们的字符串长度是奇数的时候,那么很显然中间点应该是这样的:
这时候的中间点是指向一个元素的.
我们分析完上面两种情况之后,我们基本就能分析得到哪些位置是可以作为中间点的了,就如下图所示:
可以看到除了队头以及队尾元素,其他的位置都是可以作为我们的中间点的.知道了上面这些概念之后,我们的思路就已经讲解完毕了.
源代码:
class Solution { //中心扩散法 public String longestPalindrome(String s) { int max=1; String result=s.substring(0,1); for(int i=0;i<s.length()-1;i++) { //长度为奇数的最大回文串 String oddString=diffusion(s, i, i); //长度为偶数的最大回文串 String evenString=diffusion(s, i, i+1); String maxString=oddString.length()>evenString.length()?oddString:evenString; if(maxString.length()>max) { max=maxString.length(); result=maxString; } } return result; } //扩散函数,找到以当前位置为中间点的最大回文串 public String diffusion(String s,int left,int right) { int i=left; int j=right; while(i>=0&&j<s.length()) { if(s.charAt(i)==s.charAt(j)) { i--; j++; } else { break; } } return s.substring(i+1, j); } }
Z形变换
题目描述:
将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:“PAHNAPLSIIGYIR”。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
示例 1:
输入:s = “PAYPALISHIRING”, numRows = 3
输出:“PAHNAPLSIIGYIR”
示例 2:
输入:s = “PAYPALISHIRING”, numRows = 4
输出:“PINALSIGYAHRPI”
解释:
示例 3:
输入:s = “A”, numRows = 1
输出:“A”
解题思路:
这题属于看上去难,但是找到思路之后就会很简单的题.这里我们可以将整个字符串划分成一份一份的来看.假设我们的numRows为4的话,那么可以将字符串以2*numRows-2=6为一份,就如下图所示:
分成这样的一份一份的之后,我们就需要解决下面一个问题:怎么将对应的元素塞入对应的行里面的,也就是怎么快速计算每个字符对应的行号呢?
这里其实我们想想就能知道,这就是个找规律的问题,上面我们已经将字符串拆分成一份一份了,那么很显然我们就可以直接通过%(2*numRows)的方式来计算出每个字符对应的位置但是这里还需要注意一点,这个我在下面的图里面已经写出来了.即下图所示:
这样我们的序列就能够正常的添加到相应的字符串队列中了,最后我们只需要按照顺序将我们的字符串添加起来就行了.
源代码:
class Solution { //判断每个字符应该添加在哪一行 public int row(int index,int numRows) { if(index%(numRows*2-2)<numRows) return index%(numRows*2-2); else return 2*numRows-2-index%(numRows*2-2); } public String convert(String s, int numRows) { if(numRows==1) { return s; }else { //创建numRows个字符串用来存储我们的元素 StringBuffer []stringBuffers=new StringBuffer[numRows]; for(int i=0;i<numRows;i++) { stringBuffers[i]=new StringBuffer(); } //根据计算出来的行号,将元素添加到对应行号的字符串中 for(int i=0;i<s.length();i++) { stringBuffers[row(i, numRows)].append(s.charAt(i)); } //将之前我们已经分配好元素的字符串按照顺序进行拼接 StringBuffer stringBuffer=new StringBuffer(); for(int i=0;i<numRows;i++) { stringBuffer.append(stringBuffers[i]); } return stringBuffer.toString(); } } }