先刷了一遍剑指Offer,许多题目用的是自己的“土办法”。这里总结了一下,结合自己的解法和题解中一些大佬的解法,形成了对一道题目的的分析,包括巧妙的数据结构,常用的算法思想,冷门的api以及固定的套路和牛逼的技巧。
题目:数字范围在0~n-1的长度为n的数组,某些数字重复,返回任意重复的元素
分析:
for(int i = 0; i < nums.length; i++){
while (nums[i] != i){
if(nums[i] == nums[nums[i]])
return nums[i]; //即出现了重复数字,直接返回
swap(num[i],num[num[i]]); //将数值和下标对应,也就是num[i]换到下标位num[i]的位置
}
}
if(set.contains(num)) return num; //用set检查,遇到重复则返回
题目:行列均递增的二维数组,返回是否包含某个元素target
分析:
private boolean dfs(int[][] matrix, int target, int i, int j, boolean[][] flag) {
//边界判断
if(matrix[i][j] == target) return true;
if(!flag[i][j]){
flag[i][j] = true;
return dfs(matrix, target, i + 1, j, flag)||dfs(matrix, target, i, j + 1, flag);
}
}
int curx = 0, cury = col - 1; //row表示行数,col表示列数,从右上角开始搜索
while (curx < row && cury >= 0){ //curx和cury表示当前正在遍历的位置
if(matrix[curx][cury] == target) return true;
if(matrix[curx][cury] < target) curx ++;
else cury --;
}
题目:m*n字符矩阵,寻找word是否存在该字符矩阵中(有连续串串起来)
分析:标准dfs+回溯(三段式)
for(int i = 0; i < row; i ++){
for(int j = 0; j < col; j ++)
if(find(i, j, word, used, board, 0))
return true;
} //主方法遍历,因为不知道递归到底从哪个i和j开始
//1.边界判断+值判断
//2-1.标记位置1(也可在原数组标记,如将字符置'\0'空,要求原数组不能有'\0')
boolean res = find(i - 1, j, word, used, board, pos + 1) || find(i + 1, j, word, used, board, pos + 1) || find(i , j - 1, word, used, board, pos + 1) || find(i, j + 1, word, used, board, pos + 1); //3.递归执行
//2-2.标记清除
return res;
题目:m*n矩阵,从[0,0]移动,移动一格,不能进入行列坐标的数位之和大于k的格子,返回可以到达格子数量
分析:dfs+剪枝(只用考虑向下和向右)+标志位记录(标志位统计和通过返回值统计) 数位之和的处理 当然bfs也能做:队列(元素为dfs的递归状态,入队为dfs中的子dfs步骤,同样需要标记,判断)(dfs流程简单,优先使用)
private int sumsp(int x){ //数位之和
int add = 0;
while(x != 0) {
add += x % 10;
x = x / 10;
}
return add;
}
// 1.标记位记录:最后在主方法遍历标志数组,统计数量
// 2.返回值记录:private int dfs(){return 1+dfs()+dfs();}
题目:对于[3 4 5 1 2],输出最小值1,注意原数组是非递减
分析:
while(l < r){
int mid = (l + r) / 2; //由一般到特殊,但是规律最好覆盖情况多,否则各种if-else判断
if(numbers[mid] > numbers[l]){
l = mid;
}else if(numbers[mid] < numbers[l]){
r = mid;
}else{
if( numbers[l] == numbers[l+1]) l ++; //1.左边一直连等[2 2 2 0 1]
else if(numbers[r] == numbers[r-1]) r --; //2.右边一直连等[10 1 10 10 10]
else l ++; //3.自身[1 3] [3 1]
}
}
return Math.min(numbers[0], numbers[r]); //因为比的是左边,极易跳过左边值
while(l < r){
int mid = (l + r) / 2;
if(numbers[mid] > numbers[r]){
l = mid + 1;
}else if(numbers[mid] < numbers[r]){
r = mid;
}else{
r --;
}
}
return numbers[l];
题目:输入一个数组,要求使数组中的所有奇数位于所有偶数之前
分析:如果利用辅助数组,很直观;如果在原数组交换,则有两种思路:
1)前后指针
while (l < r){
while (l < r && (nums[l] & 1) == 1) l ++;
while (l < r && (nums[r] & 1) == 0) r --;
if(l < r) swap(nums, l, r);
}
2)从头出发的快慢指针
while(f < nums.length){
while(f < nums.length && (nums[f] & 1) == 0) f ++;
if(f < nums.length) swap(nums, s, f);
s ++;
f ++;
}
总结:前后指针更易理解,快慢指针思路非常巧妙
题目:顺时针从外到内打印矩阵
分析:第一次的做法是用四个变量记录状态,依次完成左右——上下——右左——下上,但是状态其实边界判断是有点复杂的,对行列奇数和偶数的判断不同,在一个就是并不是所有矩阵都一定按上面4步走,肯能有重复数字。
第一次做法有一种均分思想,[1 2 3][4 5 6][7 8 9] 就是[ 1 2] [1 2 3 6] [1 2 3 6 ] [1 2 3 6 9 8],这样会漏
改进做法有贪心的感觉,尽可能多的去输出 [1 2 3] [1 2 3 6 9] [1 2 3 6 9 8 7]
while (index != len){
for(int i = colleft; i <= colright; i ++) res[index ++] = matrix[rowup][i];
for(int i = rowup + 1; i <= rowdown; i ++) res[index ++] = matrix[i][colright];
if(index < len){
for(int i = colright - 1; i >= colleft; i --) res[index ++] = matrix[rowdown][i];
for (int i = rowdown - 1; i > rowup; i --) res[index ++] = matrix[i][rowup];
}
colleft ++;colright --;rowup ++;rowdown --;
}
题目:有一个数字,重复次数超过了数组长度一半,求这个数字
分析:要是用map统计,对不起超过一半这个条件,很明显,题目不想用额外的空间。超过一半就意味着删掉一半元素,还有这个数存在,如果每次都能删除两个不同的数字,留下的一定就是答案;直观想到用一个used数组记录有没有删,然后左右两指针去对比,或者都从左边出发,
int p = 0; int cur = 1;
while (cur < nums.length){
if(nums[cur] != nums[p]){
use[cur] = true;
use[p] = true;
while (use[++p]);
while ((++cur) == p);
}else cur ++;
}
return nums[p];
用了多余空间,极大可能不是最优,最优为摩尔投票法:票数正负抵消,对拼消耗。
int x = 0, votes = 0;
for(int num : nums){
if(votes == 0) x = num; //为0才开始新的对拼,不为0说明有一个值出现了多次且没有被抵消完
votes += num == x ? 1 : -1;
}
return x;
题目:设计一种数据结果,实现添加元素和求中位数两个功能
分析:一种思路是排序,然后选择
不好想,利用双队列,回过头想,中位数就是把数据分成了两组,一组大的,一组小的
private PriorityQueue<Integer> maxHeap, minHeap;
public MedianFinder() {
big = new PriorityQueue<>(); //小到大,大一半数据
small = new PriorityQueue<>(Collections.reverseOrder()); //大到小,小一半数据
}
public void addNum(int num) {
small.offer(num);
big.offer(small.poll()); //把最大值拿出来入队
if (big.size() > small.size()) small.offer(big.poll());
}
public double findMedian() {
return big.size() != small.size() ? small.peek() : (small.peek() + big.peek()) / 2.0;
}
题目:输入整形数组,输出最大连续子数组和
分析:容易想到Cn2种情况,一一求和判断;细想下来,最终结果的第一个数和最后一个数一定不是负数,除非数组全是负数,假如到第m项,要不要加第m项,一种是尽管负数,但是之后是正数可能导致更大,或者正数加或者负数不加。
假设dp[i]表示以i结尾最大值,那么dp[i+1] = max{dp[i] + nums[i+1], nums[i+1]},然后遍历即可
for (int i = 1; i < nums.length; i++) {
dp[i] = dp[i-1] > 0 ? dp[i-1] + nums[i] : nums[i];
}
题目: m*n 二维数组,右上角出发,只能走右或下,走到右下角的最大收益,即路径和
分析:拆分问题,如果是2*2,那么不是(1,1)–>(1,2)–>(2,2)就是(1,1)–>(2,1)–>(2,2),即val(2,2) + max{add(2,1),(1,2)}就是最大的add(2,2),add就表示到某点的路径和。
先指定边界条件,然后开始表演
add[i][j] = Math.max(add[i][j - 1], add[i - 1][j]) + val[i][j]; //为了省空间,也可以在原数组加
题目:数组中任意两个数,如果下标小的数值大,那么就构成逆序对,输出总的逆序对数
分析:两次遍历,是暴力解法,肯定要寻求更优法。很容易想到分治,如果把原数组分成两半,假如各自一半都求出组内有多少,那么一个元素来自左半边,一个元素来自右半边该怎么求?如果还是两轮for循环,似乎并没有减少复杂度,因为O(n2/4),但如果先排序,就只用扫一遍,就是O(n2logn/2/2),似乎更复杂了…
如果能排序时候就达到扫一遍统计的效果,那么就能减少复杂度,其实归并排序的合并就能同时达到统计效果
for (int k = l; k <= r; k++) { //l是左边界,r是右边界,tmp组数可认为是小组内排好序,nums真正原数组
if (i == m + 1) //i的初值为l,m是左半边的边界,即左边的数到头了且最后一个也扫过了
nums[k] = tmp[j++]; //
else if (j == r + 1 || tmp[i] <= tmp[j]) //右边的数到头了且最后一个扫过,或者左边小于右边
nums[k] = tmp[i++];
else { //右边大于左边,且都没到头(end并未扫进nums数组)
nums[k] = tmp[j++];
res += m - i + 1; // 统计逆序对
}
}
另一方面,也可以不自己写排序,勉强能过
Arrays.sort(l); //分治后的数组
Arrays.sort(r); //直观比较逻辑,不管排序逻辑,虽然复杂度高,但是自己写的代码会少一点
int i = 0, j = 0;
while(i < l.length){
if(l[i] <= r[j]) i ++;
else{
midcount = midcount + l.length - i; //累加
if(++j >= r.length) break;
}
}
题目:在排序数组中统计某个数字的出现次数
分析:线性扫描简单直接,如果二分,就要先找到该元素,然后左右统计次数。更好的是直接定位到左、右边界
while(l <= r) {
int m = (i + j) / 2;
if(nums[m] <= target) l = m + 1; //l会一直加到r+1,此时r就是右边界
else r = m - 1; //此时中间值是大于target,所以目标值一定在mid左边
}
int right = l;
while(l <= r) {
int m = (l + r) / 2;
if(nums[m] < target) l = m + 1; //目标值一定在mid右边
else r = m - 1; //要找到左边界
}
int left = r;
return right - left + 1;
题目:数组中有两个数只出现了一次,输出这两个数字
分析:没有好办法了就是map存一下,在遍历。但题目要求空间复杂度是O(1),意味着只能有临时变量出现,对于同一个数字,进行异或运算,输出是0,0和任意数字异或还是自身,如果是只有一个元素出现一次,直接异或的结果就是该元素,到是有两个。
很关键的是对原数组按某个特征分组,使之成为一个元素出现一次的问题。这两个元素只要不相同,一定有某一位是不同的,这时候,就可以按这一位对数组划分。问题是不知道这两各元素。但是把这两各元素异或,找不为0的就是特征
for (int num : nums) res1 ^= num;
int idx = 1;
while ((res1 & idx) == 0) idx <<= 1;
for (int num : nums) {
if((num & idx) != 0) a ^= num;
else b ^= num;
}
题目:输入一个数组,只有一个数字出现1次,其余三次,输出该数字
分析:aaa还是自身,所以异或走不通;没办法,一题一个小技巧,去累加,然后对3求余,也不行,得把各数字变成2进制后,用数组统计每一位的的数量,如果是10进制,理论也行,但是比如[6 6 6 3],累加个位数是21%3=0,说明不行,那么三进制行不行,应该是可以的,因为,每一位先累加%3和2进制效果相似
for (int num : nums) {
int idx = 1;
int i = 0;
while (num != 0){
rec[i ++] += (num & idx); //统计每一位多少次
num >>= 1;
}
}
for (int i = 0; i < rec.length; i++) {
rec[i] %= 3;
res += rec[i]*idx;
idx <<= 1;
}
另外,也可以寻找到位运算的表达式,用有限状态机思想,具体参考题目后面的解题大佬的分析
题目:排序数组,输出任意一对和为s的数组
分析:不排序,就用map,既然排好序,滑动窗口肯定可以用,
while(l <= r){
int add = nums[l] + nums[r];
if(add == target){
res[0] = nums[l];res[1] = nums[r];
break;
}else if(add < target) l ++;
else r --;
}
题目:正数按序列从小到大,使之累加和为taget,输出所有满足条件数组
分析:直接可以想到就是列举,从1开始,能不能满足,再从2开始,依次到target/2,但是这样有点复杂,可以这样,(i+x)/2*(x-i+1) 解出x是不是整数且满足小于等于target/2;另外思路就是滑动窗口
while (i <= target / 2) {
if (sum < target) {
sum += j; //小于肯定要右边界扩张
j++;
} else if (sum > target) {
sum -= i; //大于肯定要左边界扩张
i++;
} else {
res.add(IntStream.range(i, j).toArray()); //j是不包括的,因为在小于target时会j++
sum -= i; //记录之后,整体往右推
i++;
}
}
题目:输入一个int数组,指定滑动窗大小,求各窗内最大值,输出为数组
分析:又是可以双for循环暴力求解,要优化,需要寻找前后窗的关系。
[a_m, a_m+1, …, a_m+n]的最大值是val_n,接下来的状态是[a_m+1, …, a_m+n, a_m+n+1],最大值val_n+1
只看这两个状态,只需关注最大值是不是a_m,不是就比较a_m+n+1,但问题是如果是,那么a_m出窗口后,谁最大,所以需要维护一个以当前窗口内最大值为队首,然后,该元素之后的值按降序排列,如果q1,q2排号,出现q3大于q2,那么就该让q2出队,q3进队,当窗口滑动,就需要判断当前队首是否是出队元素,是则出队。
for(int i = k; i < nums.length; i++) { //此时形成窗口
if(deque.peekFirst() == nums[i - k]) //是否要出队
deque.removeFirst();
while(!deque.isEmpty() && deque.peekLast() < nums[i]) //确保降序排列
deque.removeLast();
deque.addLast(nums[i]); //加新进来的元素
res[i - k + 1] = deque.peekFirst(); //输出窗口内最大值
}
题目:扑克牌1-13就是本身,大小王是0可以替代任意数字,抽5张牌,输出是否数组组成顺子
分析:很直观的分析是排序,统计出大小王个数,从i+1开始遍历,是否比前一个元素大1且小于14,不是的话,有大小王则替代,并让该位=前一位+1
while (count < nums.length && nums[count] == 0) count ++;
for(int i = count + 1; i < nums.length; i ++){
if(nums[i] == nums[i - 1] + 1) continue;
else {
if(count > 0){
count --;
i --;
nums[i] += 1;
}else return false;
}
}
再进一步想,已经明确5张牌,那么说明最大-最小<5,那么只需判断有没有重复,没有情况下,找到最大值和最小值(0除外),去重可以由set做,if(set.contains(num)) return false
题目:返回成绩数组,要求对下标i,对应的乘积数组不包含自己,即对[1 2 3],返回[6 3 2]。注意不能用除法
分析:能使用除法就算一次总积,除以各个元素就可以了;不能用除法一种做法是对每一个数,我都×其它n-1个数,显然有重复的多数相乘;另一种做法是尽可能的寻求可以共用的乘积数组部分,以dp的思想去考虑,画出二维矩阵,即相乘示意图,会形成两个金字塔形的计算,即下一步会利用上一步的结果,在乘某个数
public int[] constructArr(int[] a) {
int[] res = new int[a.length];
Arrays.fill(res, 1);
for (int i = 1; i < a.length; i++) res[i] *= res[i - 1] * a[i - 1];
int temp = 1;
for(int i = a.length - 2; i >= 0; i --){
temp *= a[i + 1];
res[i] *= temp;
}
return res;
}
题目:把字符串s中的每个空格替换成"%20,返回替换后的字符串
分析:
for (int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ' '){
sb.append("%20");
}else {
sb.append(s.charAt(i));
}
}
题目:输入是一个字符串,输出是是否表示数值 【】表示必有,[]表示可选
数值类型:[若干空格]【一个小数或整数】[一个e或E后面后面跟着一个整数][若干空格]
小数类型:[+或-]【至少一位数字+’.‘|至少一位数字+’.‘+至少一位数字|’.‘+至少一个数字】
整数类型:[+或-]【至少一位数字】
分析:这个题和正则表达式题目最大的不同在于:状态是单一的,不存在某一部分既是这样,又是那样。就比如,去除首位空格后,先判断有没有e或者E,有的话,二分判断前后两端字符串是否满足状态;如果没有,就只可能是一个小数或整数,除此之外没有其它情况
public boolean isNumber(String s) {
if(s.indexOf('e') != -1 || s.indexOf('E') != -1){ //包含e或E,按这个切分字符串
int p = s.indexOf('e') != -1 ? s.indexOf('e') : s.indexOf('E');
return isDigital(s.substring(0, p)) && isDigital2(s.substring(p + 1, s.length()););
}
return isDigital(s); //主方法到此结束
}
private boolean isDigital2(String s1) { //判断是不是整数,因此e后面是整数
if(s1.indexOf('.') == -1){
for (int j = 0; j < s1.length(); j++) {
if (j == 0 && (s1.charAt(0) == '+' || s1.charAt(0) == '-')) {
if(s1.length() == 1) return false;
} else if (s1.charAt(j) - '0' < 0 || '9' - s1.charAt(j) < 0){
return false;
}
}
return true;
}
return false;
}
private boolean isDigital(String s1) {
int i = s1.indexOf('.');
if(i == -1) return isDigital2(s1); //整数肯定是数字
if(i != 0) {//至少一位数字,不要要对长度限制,也即+.8是true
for (int j = 0; j < str1.length(); j++) {
if (j == 0 && (str1.charAt(0) == '+' || str1.charAt(0) == '-')) {
if(i == 1 && i == s1.length() - 1) return false;
} else if (str1.charAt(j) - '0' < 0 || '9' - str1.charAt(j) < 0) {
return false;
}
}
}
if(i != s1.length() - 1) {//.后面还有数字,不考虑正负号
String str2 = s1.substring(i + 1, s1.length());
for (int j = 0; j < str2.length(); j++) {
if (str2.charAt(j) - '0' < 0 || '9' - str2.charAt(j) < 0) {
return false;
}
}
}
return true;
}
另外一种思路就是状态机,定义状态,定义转移
状态0:初始状态,条件空格满足,就自旋该状态;条件’+‘或者’-‘满足,跳转状态1;条件’0-9’,跳转状态2;条件’.’,跳转状态4
状态1:e之前的符号位,条件’1-9’,跳转状态2;条件’.’,跳转状态4
状态2:小数点前数字位,条件’.’,跳转状态3;条件’1-9’,自选;条件’e’,跳转状态5;条件’ ',跳转状态8
状态2.5:小数点位,意味2+’.‘进入2.5;然后’d’进入3;’ '进入8;'e’进入5;【虽然这样也是可以的,注意下标】
状态3:小数点后数字位,条件’1-9’,跳转状态3;条件’ ‘,跳转状态8;条件’e’,跳转状态5
状态4:无小数点的数字位,如果’1-9’,跳转状态3【独立处理.开头的情况】
状态5:e和E位,如果条件‘±’,跳转状态6;如果条件’1-9’,跳转状态7
状态6:e之后符号位,条件’1-9’,跳转状态7
状态7:数字位,条件’1-9’,自选;条件’ ',跳转状态8
状态8:空格,条件‘ ’,自选
当然:考虑冗余不意味着错,先做出来----再优化合并状态。可有可无是必须单独列状态的。
Map[] maps = {
new HashMap<Character, Integer>(){{put(' ', 0); put('s', 1); put('d',2); put('.', 4);}}, //0
new HashMap<Character, Integer>(){{put('d', 2); put('.', 4);}}, //1
new HashMap<Character, Integer>(){{put('.', 3); put('d', 2);put(' ', 8);put('e', 5);}}, //2
new HashMap<Character, Integer>(){{put('d', 3); put('e', 5);put(' ', 8);}},`//3
new HashMap<Character, Integer>(){{put('d', 3);}}, //4
new HashMap<Character, Integer>(){{put('s', 6); put('d',7);}}, //5
new HashMap<Character, Integer>(){{put('d', 7);}}, //6
new HashMap<Character, Integer>(){{put('d', 7); put(' ', 8);}}, //7
new HashMap<Character, Integer>(){{put(' ', 8);}} //8
};
总结:最初把小数点当作一个状态,‘s’遇到’.’,就跳转该状态,那么该状态能不能输出就是个大问题。解决的方法就是’.‘不能做为一个状态,把’1.2’和’.2’和’1.‘这三种情况分开,也就是【‘d’+’.’】进入3,单纯的【’.’】进入4,再接数字进入3,这个处理相当巧妙!
题目:返回不包含重复字符的子字符串长度
分析:可以用一个长度128的int[]记录按字符按ASCII,从头开始滑动指针pr,依次记录在数组,当发现某位有值,说明遇到了重复字符,统计长度,然后另一个指针pl,开始移动,直到出现重复的哪个字符,之后pr继续移动
优化:对于pl,其实可以用map存位置,这样不用遍历。不是那么好理解,但是简洁
int pl = -1, res = 0; //等于-1是因为防止不进入if,此时,相当于pr-pl+1
for(int pr = 0; pr < s.length(); pr ++) {
if(map.containsKey(s.charAt(pr)))
pl = Math.max(pl, map.get(s.charAt(pr))); // pl指针要一直右移
map.put(s.charAt(pr), pr);
res = Math.max(res, pr - pl);
}
题目:比如"abaccdeff",输出第一个只出现一次的字符,就是b
分析:首先肯定得扫描完才能判断,可以考虑用按插入顺序排序的的map来存
Map<Character, Boolean> dic = new LinkedHashMap<>();
for(char c : chs) map.put(c, !map.containsKey(c));
如果不考虑顺序,那么可以先扫一遍存map,再扫一遍,看谁的value满足要求也行
题目:去掉前后空格,字符串按空格切分后倒序输出,切分后的单个字符串顺序不变
分析:直接切分,然后从后到前拼即可
String[] s1 = s.trim().split("\\s+");
for (int length = s1.length - 1; length >= 0; length--) {
buffer.append(s1[length] + " ");
}
return buffer.toString().trim();
另外就是不适用切分方法,双指针从后扫到头
while(i >= 0) { // i = j = s.length() - 1; 在次之前先trim
while(i >= 0 && s.charAt(i) != ' ') i--; // 此时的i一定对应空格
res.append(s.substring(i + 1, j + 1) + " "); // i+1是单词的开始
while(i >= 0 && s.charAt(i) == ' ') i--; // 此时的跳出while的i一定是单词的结尾或者i<0了
j = i; // 如果是找到下一个单词的尾,则j移动到单词的尾部,开始新的while去定位到头,然后输出......
}
题目:在字符串第某个下标前的字符串移动到末尾
分析:关于旋转,一般先拼接,在操作很方便
return (s+s).substring(n, n + s.length());
另外,也可以利用字串拼接
return s.substring(n, s.length()) + s.substring(0, n);
参考该题目的题解区,求余操作,很秀,和第一种异曲同工之妙
for(int i = n; i < n + s.length(); i++) res += s.charAt(i % s.length());
题目:输入字符串,它可能以空格开头,需要找到第一个非空字符,并且可能该字符是±,要把这之后的数字组合起来,出现的非数组应该被忽略,其它情况返回0。注意:返回时int类型,所以超过边界的返回边界即可
分析:这种转换像一种面向过程,面向情况的分析。可以遵循下面步骤:1.找到第一个非空字符下标,判断是否超过数组边界;2.判断是否±开头;3.去除符号位之后的0;4.遍历,得到非数字的下标;5.对该数组做边界判断(即和int最大值和最小值判断)
char[] chs = str.trim().toCharArray();
if(chs.length == 0) return 0;
int res = 0, bndry = Integer.MAX_VALUE / 10; //做边界判断-2147483648 ~ 2147483647
int i = 1, sign = 1;
if(chs[0] == '-') sign = -1;
else if(chs[0] != '+') i = 0;
for(int j = i; j < chs.length; j++) {
if(chs[j] < '0' || chs[j] > '9') break;
if(res > bndry || res == bndry && chs[j] > '7') return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE; //是7就还可以*10
res = res * 10 + (chs[j] - '0'); //省去了第3步
}
return sign * res;
值的学习的点:对于整型边界判断,先除以10,否则如果从长度判断,比如可以大于10的直接输出边界,然后用Long来处理小于10的,就会稍微麻烦一点。
题目:数组返回从链表尾部到头部序列
分析:
private void rec(ListNode head) {
if(head == null) return;
rec(head.next);
list.add(head.val); //list是成员变量ArrayList
}
public ListNode rec(ListNode node){ // a---->b---->null
ListNode nextNode = node.next; //取出当前遍历的下一节点
if(nextNode != null){ //不为null说明没有到最后一个节点
ListNode newHead = rec(nextNode); //返回了最后一个节点
nextNode.next = node; //a如果是cur,那么nextNode就是b,把b指向a
node.next = null; //a指向空,否则,当回到首元素,首元素还指向第二个元素,成循环链表了
return newHead;
}
return node; //永远返回最后一个节点
}
list.stream().mapToInt(Integer::valueOf).toArray(); //用List存储,再转换成int[]
ListNode pre = null;
while(head != null){
ListNode next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
题目:删除链表某个节点,返回删除后的链表头节点
分析:双指针
while(cur != null && cur.val != val){ //1---->3---->4【删除】---->5
pre = cur; //最后一次pre = 3,如果没有就会一直到pre=5,cur = null跳出
cur = cur.next; //最后一次cur = 4
}
题目:输出链表倒数第k个节点,从下标1开始
分析:最直观的想法是计数,但是,利用双指针思想,相隔k个,当后面指针指向null,前面指针就是输出
while(cur != null){
if(n ++ >= k) res = res.next;
cur = cur.next;
}
题目:合并两个排好序的链表
分析:主要就是对于null指针的处理,比如:链表1null,俩表2非null;链表1非null,俩表2null;都非null包含三种情况,每种情况里面,还得先直到我的新头节点(要返回的节点)到底有没有赋值。
逻辑会比较繁琐,遇到是否要判断新头节点是否赋值,比较好的办法是创建preHead,即头节点之前的一个节点,无论什么情况,只需返回preHead.next即可。
ListNode preHead = new ListNode(0), cur = preHead;
while(l1 != null && l2 != null) {
if(l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
}
else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = l1 != null ? l1 : l2; //不要放在while循环内部
return preHead.next;
题目:普通的单向链表+每个node有有随机指针指向该链表中某一个节点上
分析:单向链表基础上再做随机指针的话,对于后指向前,前的定位不容易做,按理可以从头节点遍历找到。也就是说结构如何复制呢?可以给每个原始链表的节点node后面复制该节点node_copy添加到node后面,这样做好处就是直接给random节点之后插入复制random的节点,最后进行拆分该链表。其实,可以用map对这个原理进行封装,key为原链表node,value为复制链表node_copy,然后要拿出来random对应节点就拿出来了
Node cur = head; Node preHead2 = new Node(0); Node preCopy = preHead2;
while (cur != null){
Node curCopy = new Node(cur.val);
map.put(cur, curCopy);
preCopy.next = curCopy;
preCopy = curCopy;
cur = cur.next;
}
cur = head;
while (cur != null){
Node randNode = cur.random;
Node curCopy = map.get(cur);
Node randCopy = map.get(randNode);
curCopy.random = randCopy;
cur = cur.next;
}
问题:输入两个链表,求第一个公共节点
分析:扫一遍第一个,set记录一下,然后扫另一个,可以达到目的。有小技巧,第一个链表单独的长度为a,第二个为b,公共为c,那么不妨设a>b,然后b先扫完,然后移到a开始的地方,经过a+b+c就在公共节点相遇了
while (A != B) {
A = (A != null ? A.next : headB);
B = (B != null ? B.next : headA);
}
题目:输入的前序遍历和中序遍历(不包含重复数字),返回重建二叉树
分析:
//成员变量存前序遍历数组和map存储中序遍历数组
/*难点在于:在中序数组分割后,子问题数组开始和结束,前序遍历数组子问题开始和结束,理论需要四个下标位置,前序遍历子问题数组的开始一定是子问题的根,它的结束并不关心,其次要传递中序遍历的开始位置和结束位置。当划分子问题时,左子数容易得出这三个条件,右子树最难的是pre数组开始,即子问题根,记位置为x:
preorder =[3,9,20,15,7]
inorder = [9,3,15,20,7]
举例子从特殊到一般是一种好方法,单容易出现找到的是个例的规律,就比如,容易认为这两个数组的后三个元素对应了右子树,从而断下标x=pos+1
实际上:root位置|左子树root|左子数其它|右子树root【x】|右子数其它
【left】左子树|root位置【pos】|右子树【right】
也就是x = root + 左子数元素个数 = root + pos - left + 1
*/
recur(0, 0, inorder.length - 1); //主方法调用
TreeNode rec(int root, int left, int right) {
if(left > right) return null;
TreeNode node = new TreeNode(preorder[root]); // 建立根节点
int pos = map.get(preorder[root]); // 划分根节点
node.left = rec(root + 1, left, pos - 1); // 开启左子树递归
node.right = rec(root + pos - left + 1, pos + 1, right); // 开启右子树递归
return node;
}
题目:判断树A是不是树B的子结构
分析:指明了B一定是子树,因此,只需从A的根开始一一比对,递归比较,再比较根的左右孩子,依次递归2
递归的模式:递归终止条件+递归(不满足终止条件则继续递归)
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(A != null && B != null){
if(rec(A, B)) return true; //递归1
else return isSubStructure(A.left, B) || isSubStructure(A.right, B); //递归2
}
return false;
}
boolean recur(TreeNode A, TreeNode B) {
if(B == null) return true;
if(A == null || A.val != B.val) return false;
return recur(A.left, B.left) && recur(A.right, B.right);
}
题目:输出一个二叉树的镜像
分析::并不是只把根节点的左右孩子交换,而是孩子的孩子也需要交换,即交换是递归的
//类似两数交换
if(root == null) return null;
TreeNode l = root.left;
root.left = mirrorTree(root.right);;
root.right = mirrorTree(l);
return root;
题目:一颗二叉树是否对称
分析:从根节点,首先判断左右孩子是否相等,然后判断左.左 == 右.右,左.右 == 右.左,至此三层以内相等;然后在判断左.左.左 == 右.右.右 左.左.右 == 右.右.左
穷举显然不切实,如果总局限在根节点1个节点,发现除了一一列举,没办法。如果把判断过程进来用数学去描述,不难发现共同的点,即第四层要去除第一个左或者右之后,和第三层判断是一样的。所以,相似的逻辑就是递归执行逻辑,不同的地方就是递归的入口条件不同
rec(root, root); //需要加递归的出口判断
if(a.val == v.val) return rec(a.left, b.right) && rec(a.right, b.left); //递归逻辑
题目:输出为一维数组——>输出为包含层信息的二维数组——>之字型打印
分析:难点在于2个,1个是怎么直到这层打印完了,另一个是怎么直到这层是奇数还是偶数
第一个问题,可以把队列元素全取出来,然后放入的全是下层元素;第二个在循环中维护层信息或者直接看当前list>的元素个数。
while(!queue.isEmpty()) {
LinkedList<Integer> tmp = new LinkedList<>();
for(int i = queue.size(); i > 0; i--) { // 用的超好,因为后面会让queue.size()变化
TreeNode node = queue.poll();
if(res.size() & 1) == 0) tmp.addLast(node.val); // 偶数层 -> 队列头部
else tmp.addFirst(node.val); // 奇数层 -> 队列尾部
if(node.left != null) queue.add(node.left);
if(node.right != null) queue.add(node.right);
}
res.add(tmp);
}
题目:判断数组是否是搜索二叉树的后续遍历结果
分析:明显就是递归,以[1,3,2,6,5]为例,5肯定是根节点,那么从头找最后一个小于5的,有可能没有,所以注意边界,这里是2,以[1 3 2]为新树去递归,然后检查一下2之后是不是都大于5,满足的话就把[6]做新树去递归
public boolean verifyPostorder(int[] postorder) {
if(postorder.length <= 1) return true;
int l = -1;
for (int i = 0; i < postorder.length; i++) {
if(postorder[i] > postorder[postorder.length - 1]){
l = i;
break;
}
}
if(l != -1){
for (int i = l; i < postorder.length - 1; i ++)
if(postorder[i] < postorder[postorder.length - 1]) return false;
}else l = postorder.length - 1;
int[] larr = Arrays.copyOfRange(postorder, 0, l);
int[] rarr = Arrays.copyOfRange(postorder, l, postorder.length - 1);
return verifyPostorder(larr) && verifyPostorder(rarr);
}
//当然这样内存也太浪费了,纯粹是想用一个函数解决
public boolean verifyPostorder(int[] postorder) {
if(postorder.length <= 1) return true;
int l = 0;
while(postorder[l] < postorder[postorder.length - 1]) l++;
int mid = l;
while(postorder[l] > postorder[postorder.length - 1]) l++;
int[] larr = Arrays.copyOfRange(postorder, 0, mid);
int[] rarr = Arrays.copyOfRange(postorder, mid, l);
return l == postorder.length - 1 && verifyPostorder(larr) && verifyPostorder(rarr);
}
//应该这样来写
public boolean rec(int[] postorder, int left, int right) {
if(right - left <= 1) return true;
int l = left;
while(postorder[l] < postorder[right-1]) l++;
int mid = l;
while(postorder[l] > postorder[right-1]) l++;
return l == right-1 && rec(postorder,left, mid) && rec(postorder, mid, right - 1);
}
题目:从root开始到叶子节点之后等于target的所有路径输出
分析:递归加,递归左右孩子,因为要用list存,如果是函数参数变量,涉及到了回溯,因为一直用这个list记录
涉及递归记录状态的题目,要么把状态保存在类成员变量,要么用函数参数传递
private void rec(TreeNode root, int now) {
if(root == null) return;
list.add(root.val);
if(now + root.val == target && root.left == null && root.right == null)
lists.add(new ArrayList<>(list));
rec(root.left, now + root.val);
rec(root.right,now + root.val);
list.removeLast();
}
题目:输入一棵二叉搜索树,要求一个排好序的双向链表(在树上修改)
分析:二叉搜索树的性质是把树压扁就是排好序的,即中序遍历结果是有序的,因此这个题就是中序遍历的升级
private void dfs(Node root) { //最后要让head和pre(队尾)相连
if(root == null) return;
dfs(root.left);
if(head == null){
head = root;
pre = head;
}else {
root.left = pre;
pre.right = root;
}
pre = root;
dfs(root.right);
}
题目:实现两个函数,将树层序遍历输出,根据层序遍历结果建立二叉树
分析:该输出是包含null值的,否则只根据层序遍历的非null值无法建树;很明显,是要用队列;
序列化就依次入队,关键是左右孩子都null入不入队,这里可以先默认就入队,造成结果是比如{1 2 3}的树就会是[1 2 3 null null null null];
然后考虑建树,完整的层序是存在2n+1和2n+2关系的,既可以用递归,也可以用while循环
//序列化 node为null的就添加null值给StringBuilder
Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }}; //这种初始化简洁
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(node != null) {
sb.append(node.val + ","); //sb为StringBuilder
queue.add(node.left);
queue.add(node.right);
}
else sb.append("null,");
}
//反序列化 遇到null不处理就行 2n+1和2n+2其实在一次次循环中利用就是这个性质
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(!strs[i].equals("null")) {
node.left = new TreeNode(Integer.parseInt(strs[i]));
queue.add(node.left);
}
i++;
if(!strs[i].equals("null")) {
node.right = new TreeNode(Integer.parseInt(strs[i]));
queue.add(node.right);
}
i++;
}
题目:找到二叉搜索树的第k大节点
分析:二叉搜索树的中序遍历是有序数组,所以从右孩子遍历,再根节点,再左孩子,就是从大到小,遍历,记录即可
void dfs(TreeNode root) {
if(root == null) return;
dfs(root.right);
if(--k == 0) {
res = root.val;
return;
}
dfs(root.left);
}
另外,如何重建二叉搜索树?先排序,再递归建子数
题目:输出二叉树的最大深度
分析:只要有一个孩子不为null,就能+1,但是都不为null,就得两边都扫描,去比较谁的深度更大,因此递归
if(root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
题目:一棵树左右子树高度差不超过1,且子树也满足就是平衡二叉树,判断一颗树是否是平衡二叉树
分析:肯定要递归,因为平衡二叉树的定义都是递归定义,在递归求某数深度同时判断是否满足平衡
private int recur(TreeNode root) { //每次递归都要对当前根节点判断,实质是从树最底层依次向上判断
if (root == null) return 0;
int left = recur(root.left);
if(left == -1) return -1;
int right = recur(root.right);
if(right == -1) return -1;
return Math.abs(left - right) <= 1 ? Math.max(left, right) + 1 : -1;
}
题目:返回两个节点的最近公共祖先
分析:根结点肯定是,这时候要看一左一右,就是根结点,两左或两右,就该递归对应的左或右
写代码如何直到是左还是右,可以写专门函数,判断两个结点相对关系,即是左子树元素还是右子数元素,但是存在大量重复;下面是一种不用重复判断的写法:
if(root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null) return right;
if(right == null) return left;
return root; //
递归玩的太溜了,就相当于模拟了手工寻找的过程,从根的左出发,先寻找左孩子,如果发现=p或q了,那么left不为null,此时的情况:1.另一个是它的孩子,那么所有的left都不变,所有right都是null,最终返回;2.另一个是根结点左子数某个元素,那么就会递归回溯,直到,在某个右孩子上找到另一个,此时,就返回了root,又回到第一种情况了,继续往回递归;3.在根结点的右子树上,那么该递归会回到跟结点,开始右子树递归。总之,很巧!
题目:用两个栈实现一种队列的数据结构,从头部删除元素,从尾部添加元素
分析:
public int deleteHead() {
if (st2.isEmpty()) { //先判断st2,为空则判断st1,搬运
while (!st1.isEmpty())
st2.push(st1.pop());
}
if (st2.isEmpty())
return -1;
return st2.pop();
}
}
题目:要求实现一个栈,栈有存有所有元素的最小值,包含push,min和pop方法,时间复杂度O(1)
分析:一般的栈push和pop肯定就不用额外实现,所以主要是获取最小元素,如果遍历肯定复杂度达不到要求,但是很明显,前一时刻的最小值和下一时刻的最小值明显存在关系,min_now = min(min_pre, val_now);
但是又有个问题:出栈了,但是假设有一数组用来存最小元素的,那么就需要更新最小元素
比如用队列:第一个元素a1直接进队,第二个a2和第一个比a1,小于则将a1出队,a2进队,a1的信息是不用再存的,如果等于,也要扔进队列中,如果大于,更得扔进队列,处理的情况就是它前面的全出栈了,自然就是它最小;然后是a3,按照前面的逻辑,a3只能和队首比,大于等于的话只能入队,小于的话就得让队首出栈直到把它自己放进去。
以上分析的题目是:实现一个队列,… 忘记了出去的顺序是栈:后进先出
所以重新分析:a1先入队,轮到a2,比a1小肯定要入队,但不是出队a1,比a1大不用入队,这样,最小值就是从队尾取;来了a3,该和a2比,小于入队,等于也需要入栈
public void push(int x) {
stack.add(x);
if(queue.size() == 0) queue.add(x);else{
if(queue.peekLast() >= x) queue.add(x);
}
}
public void pop() {
if(stack.pop().equals(queue.peekLast())) queue.removeLast();
}
另外一种思路是:做一个一一对应关系,也就是都添加,都删除
queue = new LinkedList<>();
public void push(int x) {
stack.add(x);
if(queue.size() == 0) queue.add(x);
else
if(queue.peekLast() > x) queue.add(x);
}else queue.add(queue.peekLast());
}
public void pop() {
stack.pop();
queue.removeLast();
}
public int min() {
return queue.peekLast();
}
题目:给一个序列a,再给一个可能是出栈的序列b,判断是否是出栈序列
分析:比如[1 2 3 4 5] 出栈顺序会很多,[4 5 3 2 1],要判断是不是,最好的方法就是模拟这个过程,看能不能持续到出栈数组的最后一个元素。以栈解栈,否则,这个过程并不好描述。具体就是:从b第一个开始,看栈顶是不是,不是就从a取元素入栈,如果是了,就出栈,继续这个逻辑
while (out < popped.length){ //out和in从0开始,pushed对a,proposed对b
if(in < pushed.length && (stack.isEmpty() || popped[out] != stack.peek())) stack.push(pushed[in++]);}
else if(popped[out] == stack.peek()){
stack.pop();
out ++;
}else return false;
}
题目:一个无序数组,下标大的减下标小的最大值是多少
分析:两次for循环,是直观的做法;可以维护一个队列或者栈,满足递减,这样,对所有元素,都和栈顶元素做差,求极值
for (int i = 0; i < prices.length; i++) {
if(stack.isEmpty()) stack.push(prices[i]);
else if(stack.peek() > prices[i]) stack.push(prices[i]);
else res = Math.max(res, prices[i] - stack.peek());
}
其实栈的作用是保存最小值,也可以用变量替代,该变量保存扫描到当前的最小值
题目:实现一个队列,入队和出队和遵循先进先出,但是要求能返回max_value,且均摊复杂度O(1)
分析:普通队列求最值只能遍历,而优先队列保证不了先进先出,所以需要再维护一个递减队列,用来返回最值
public void push_back(int value) {
q.add(value);
while(!max.isEmpty() && value > max.peekLast()) max.pollLast(); //保证单调
max.add(value);
}
public int pop_front() {
if(q.isEmpty()) return -1;
if(q.peek().equals(max.peek())) max.poll(); //的用equals,因为是Integer类型
return q.poll();
}
public int max_value() {
if(max.isEmpty()) return -1;
return max.peekFirst();
}
题目:求第n项斐波那契数列
分析:有重叠子问题,所以还是动态规划,注意的是,答案需要取模 1e9+7(1000000007)
补充:两个数相加不爆int,两个数相乘不爆long long,1e9+7是质数
for(int i = 2; i <= n; i ++){
dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
}
分析:有一系列基于斐波那契数列的变形,只需改边界条件即可。改题目:对于上到第i级台阶,假设已知dp[i-1]和dp[i-2],则dp[i-2]上2步和dp[i-1]上一步覆盖了所有可能情况。
题目:长为n的绳子剪成m段(m和n为整数),返回m(m>1)段长度积的最大值
分析:易得dp[n] = max{dp[1]dp[n-1], dp[2]dp[n-2]…dp[n-1]dp[1]]}
但是容易遗漏:虽然dp[2]本身最大显然为2,但是作为子绳子,最大值不是dp[2],而是max(dp[2], 2)
另外,贪心也可以,当然不是太明显,直观做法还是动态规划
int[] dp = new int[n + 1]; //dp[0]不用,只是数组下标和实际n对应,如果要输出整个数组,可以从0开始
dp[1] = 1; //1.初始条件
for (int i = 2; i < n + 1; i++) {
for (int j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], Math.max(dp[j], j) * (i - j)); //2.开始根据表达式动态规划
}
}
return dp[n];
题目:I中n范围为[2,58],II为[2,1000],也就是要对大数取余,即大数越界情况下的求余问题
分析:思路一样,但是对于大数(超过int,long)处理比较麻烦
BigInteger[] dp = new BigInteger[n + 1];
dp[1] = BigInteger.valueOf(1);
Arrays.fill(dp, BigInteger.valueOf(1));
for (int i =2; i < n + 1; i++) {
for (int j = 1; j < i; j++)
dp[i] = dp[i].max(dp[j].multiply(BigInteger.valueOf(i - j)).max(BigInteger.valueOf(j * (i - j))));
}
return dp[n].mod(BigInteger.valueOf(1000000007)).intValue();
补充贪心对应的高次幂求余(可以二分加快求解速度) xa⊙p=[(x(a−1)⊙p)(x⊙p)]⊙p
(5^3⊙20 = ((5^2⊙20)(5⊙20))⊙20 = (5*5)⊙20 = 5
// 求 (x^a) % p —— 循环求余法
public long remainder(int x,int a,int p){
long rem = 1 ;
for (int i = 0; i < a; i++) {
rem = (rem * x) % p ;
}
return rem;
}
题目:返会数组[1 2 3 … 10^n-1]
分析:
return IntStream.range(1, (int)Math.pow(10, n)).toArray();
// 初始化
char[] chs = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
char num = new char[n];
// dfs
void dfs(int idx) {
if(idx == n) { //终止条件
res.append(String.valueOf(num) + ","); // 拼接 num 并添加至 res 尾部,使用逗号隔开
return;
}
for(char i : chs) { // 遍历
num[idx] = i; // 固定第 idx 位为 i
dfs(idx + 1); // 递归
}
}
题目:'.'
表示任意一个字符,'*'
表示它前面的字符可以出现任意次(含0次),返回是否字符串s和模式p匹配
分析:难点在于*可以匹配前面字符任意次,如果是0次意味着和前一个抵消了,否则,可以重复前一个1,2…次
对于一个复杂问题,不是分治,就是递归(dp),总之都要降低问题闺蜜
容易想到dp[i][j]表示s串前i个字符和p前j个字符是否匹配,dp[0][0]=true,dp[n][0]=false,dp[0][1]=false
s:0 1 2 i-2 i-1 i i+1 …
p:0 j-2 j-1 j j+1 …
case1:s[i-1]=p[j-1]|.,那么dp[i][j] = dp[i-1][j-1](注意s[i-1]表示第i个字符)
case2:p[j-1]=*,那么分情况:
case2-1:p[j-2] = s[i-1],则dp[i][j] |= dp[i-1][j-1] 即让*充当前一个元素一次,后移
case2-2:p[j-2] = s[i-1],则dp[i][j] |= dp[i-1][j+1] 即让*抵消前一个元素
case2-3: p[j-2] = s[i-1],则dp[i][j] |=dp[i+1][j] 即让*充当前一个元素一次后不移动
这种地推关系显然没有实际求解,因此难点就在于消除当前状态之后状态的影响
逆向思维:也就是从dp[m][n]出发,从最后一个字符能不能匹配开始
case1:s[i-1]=p[j-1]|.,那么dp[i][j] = dp[i-1][j-1](最后一个字符匹配了,就看各自前一个是否匹配)
case2:p[j-1]=*,那么分情况:
case2-1:p[j-2] = s[i-1],则dp[i][j] |= dp[i-1][j-1] 即让*充当前一个元素一次,后移
case2-2:直接抵消前一个元素,则dp[i][j] |= dp[i][j-2] 即让*抵消前一个元素
case2-3: p[j-2] = s[i-1],则dp[i][j] |=dp[i-1][j] 即让*充当前一个元素一次后不移动
需要说明的是:分析时单列出来了2-1,其实出现2-3情况,当前不动,下一次就移动就覆盖了case2-1,也就是说case2-1默认了只复制一次后,s的下一个元素和以前不一样,再复制肯定就不匹配,因此p直接移动
for(int j = 0; j <= lenp; j ++)
for(int i = 0; i <= lens; i ++){
if (i > 0 && j > 0 && (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.')) { //case1
flag[i][j] = flag[i - 1][j - 1];
} else if (j > 0 && p.charAt(j - 1) == '*') { //case2
if (j > 1) { //case2-2 直接抵消
flag[i][j] |= flag[i][j - 2];
}
if (j > 1 && i > 0 && (p.charAt(j - 2) == s.charAt(i - 1) || p.charAt(j - 2) == '.')) {
flag[i][j] |= flag[i - 1][j]; //case2-3
}
}
}
题目:对于字符串abc,输出{abc,acb,bac,bca,cab,cba},注意字母可能包含重复,入字符串aa,输出{aa}
分析:无重复字符串的输出就是固定的模式
if(pos >= s.length()) //到第几个数字了,到pos说明所有字符都被考略了,所以输出
list.add(sb.toString()); return; //sb用的是StringBuilder
for(int i = 0; i < s.length(); i ++){
if(!used[i]){
sb.append(s.charAt(i));
used[i] = true;
dfs(pos + 1);
used[i] = false;
sb.deleteCharAt(sb.length() - 1);
}
}
难点就在于重复字符串的处理,可以考虑set去重,另外一种通用就是先对字符排序,比如abbc,那么去重关键在于:b1和b2的位置限制,即如果b2出现在b1前面,且有数值相等关系,说明后面全是重复的
//一定要让b1先使用,否则,就说明b1用过回溯b2开始使用,此时b1没使用,就会依次排在b2后面,直接跳过
if(i > 0 && s.charAt(i) == s.charAt(i - 1)&& !used[i - 1]) continue;
举例:ab1b2b3 ab1b3. ab2… ab3… b1ab2b3 b1ab3. b1b2ab3 b1b2b3a b2… b3…
题目:输出n个骰子掷出所有情况的概率分布
分析:并不能很直观觉着2个骰子的结果和三个的结果有很直接的关系,假如不通过动态规划,那么降低问题规模的还有分治,一部分掷出x,另一部分掷出y,但是情况又根骰子数相关,关键是状态太多了
再分析动态规划,已知2骰子的分布,那么对于3骰子,对于特定点数x = 1*2点对应之和 + 2*2点对应之和 + 6…
而两点之和已经求出,那么可以dp解决,dp[i][j]表示i个骰子掷出j点的概率
for(int i = 1; i <= 6; i ++) dp[1][i] = 1.0 / 6; //Arrays.fill(dp[0], 1.0 / 6.0);
for (int i = 2; i < n; i++) dp[i][1] = 0;
for(int i = 2; i < n + 1; i ++){ //从2骰子开始,到n结束
for(int j = 1; j < 6 * n + 1; j ++){ //n个骰子所有可能情况
for(int k = j - 1; k >= j - 6; k --){ //第n个骰子分别掷出1-6
if(k > 0) dp[i][j] += dp[i - 1][k] * dp[1][j - k];
}
}
} //该题有时候会想4和2 3和3 2和4是几种情况,3和3只能对应一种,而4和2与2还4是两种
通常,二维的dp问题,实质上只依赖前一刻状态,也就是可以转化为一维数组的表示,当然二维更清晰一点
题目:输入为int类型整数n,返回对应二进制表示1个个数
分析:标准的位运算
while(n != 0){
res += (n & 1);
n = n >>> 1; //无符号右移
}
题目:实现pow(x, n)
分析:标准的仅由乘法快速计算幂 如9:1001 只需计算21次和23次
if(n < 0){
pn = -pn; //变为long类型处理(正负范围是不一样的) long pn = n
x = 1 / x; //正负的处理,可以把负数变为求1/x处理
}
while (pn > 0){
if((pn & 1) == 1)
res *= x;
x *= x; //x = x*x
pn >>= 1;
}
题目:无序数组求最小的k个数
分析:
排序,输出
快排求得k小,此时k的左边满足条件
private static int[] subKMinNumQuickSort(int[] arr, int l, int r,int k) {
int base = arr[l]; //这个基准可以随机选取
while (l < r){
while (l < r && arr[r] >= base) -- r;// 机端情况是tempBase是最小值,此时r会减到l
arr[l] = arr[r]; //此时r下标要么比base小,要么是r==l,把l处的值替换为r处的小值
while (l < r && arr[l] <= base) ++ l;//机端情况l一直增加到r,否则l下标的值大于base
arr[r] = arr[l]; //把r处的小值替换成大值
} //跳出循环意味着l == r
arr[l] = base; //可以返回分割点的最终下标,便于函数递归
}
优先队列
自己实现堆排序
int len = arr.length;
for (int i = 0; i < len - 1; i++) {
int last = (len - i - 2) / 2; // 需要开始处理的非叶子节点
for(int j = last; j >= 0; j --){
int childMaxIndex = 2*j + 1;
if(2*j + 2 < len - i && arr[2*j + 1] > arr[2*j +2]){
childMaxIndex = 2*j + 2;
}
if(arr[j] > arr[childMaxIndex]){
swap(arr,j,childMaxIndex);
}
}
swap(arr,0, len - i - 1); //把最大元素依次移到最后
}
TreeMap
利用TreeMap对key进行排序,只需map.lastEntry()就能取出key和value,其实本质类似优先队列
6. 记数排序(桶排序)
补充:建堆和插入元素
建堆:从第一个非叶子节点开始,向下满足,直到根节点也满足
插入:插入的结点加到最后,对新插入的结点进行上移操作就行
删除:用最后值替代删除值,若大于原值,则上移,若小于原值,则下移
题目:1~n中的数字共包含多少1
分析:一个一个遍历;这个题偏技巧,通常大的思想是分治,将整体分成一部分一部分考虑,这个题可以先考虑1~n中个位为1的有多少数字,此时不管其它位置是否为1,只先算个位的1,然后算十位,这样依次考虑,对应基数排序的思想
具体该怎么统计个位有多少1?个位一定为1*十位(0-9)… 最高位(0-真实最高位-1) + 最高位为最高位的情况。
比如对1234,当千位位1,此时百位(0-最到位-1)*十位(0-9),百位如果为最到位,此时十位(0-最到位-1),好像超级麻烦,灵感一现,不就是124种情况吗?0-123,如果个位大于0就是124种,个位为0就是123种
对于十位的1,1234种,明显125种,如果十位是0,则124种,如果十位大于0,则125种
十位为1,当十位大于1,此时,124不是最大值,而是129,也就是130种,等于1有125种,小于1有120种
再看百位为1,此时有200种,000-199,但是如果百位为1,显然只能到100+35种,为0的话只有100种,规律!
while(n != 0){
int y = n % 10;
n /= 10;
if(y == 0) total += n * count;
else if(y == 1) total += n * count + low + 1;
else total += (n + 1) * count;
low += count * y; //小位累加
count *= 10;
}
题目:0123456789101112…求第n位对应的数字
分析:很明显,可以先确定大的分段,因为两位数占的空间就是2*(99-10+1),三位数3*(999-100+1),所以可以先定位到底是几位数,观察数字 10 2*90 3*900 n*10^(n-1)
注意:第n位是从0开始的,也就是题目种可以让输出第0位是0这样的结果
while (n > count) { // count初始为9,start初始为1,digit初始为1
n -= count; //相减这个做法比去累加好一点,因为累加的话,最后还要再减一次,因为要超过
digit += 1;
start *= 10;
count = (long)digit * start * 9; //这一步对整数可能越界
} //跳出while循环后,直接就定位到了n为数字种的产长度
int num = start + (n - 1) / digit; //比如11 num=10,14的话 num=12 15的话 num=12
return Integer.toString(num).charAt((n - 1) % digit) - '0'; // (n - 1) % digit判断第几个
题目:非负整数数组,要求拼起来,数值最小
分析:如[3,30,34,5,9],一般许多元素,可以先看简单的两个有没有关系,如果没有三个有没有,从简单到复杂
比如3和30,明显应该303,似乎要比较3*100+30和30*10+3的大小,如果3,30个34呢?,30肯定第一个,之前3和30就是30在前,那么30和34比也是30在前,那34和3呢,是3在前,也就是可以两两比较
之后就转换成数组排序问题了
Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x)); //str是输入转String[]
题目:1, 2, 3, 4, 5, 6, 8, 9, 10, 12 …即只包含质因子2,3和5,求第n个丑数
分析:特征有点不直观,都是在之前某个数基础上×2或3或5得到,因为,一个数只能是2n3m5^v
把1放入队列,那么接下来就该2,3,5,紧接着4,6,10,此时4应该移到5前面,所以存在队列和排序
TreeSet<Long> set = new TreeSet<Long>(); //缺点是需要的额外空间太大,意味着存了用不到的大数
for(int i = 0; i < n; i ++){
temp = set.first();
set.remove(temp);
set.add(2*temp);
set.add(3*temp);
set.add(5*temp);
}
//优化,先比较,看存哪一个
for(int i = 1; i < n; i++) {
int two = dp[a] * 2, three = dp[b] * 3, five = dp[c] * 5;
dp[i] = Math.min(Math.min(two, three), five);
if(dp[i] == two) a++;
if(dp[i] == three) b++;
if(dp[i] == five) c++;
}
题目:0,1 … n-1围圈,然后从0开始,删除第m个数字,之后,从下一个位置可以统计,输出最后剩下的一个数字
分析:虽然是简单难度,规律并不好找,容易想到模拟删除过程,构建链表,但是时间复杂度是 O(nm),在这个题会超时,最朴素办法会超时,一定说明两次删除有联系或者删除是有规律的。
List<Integer> list = new ArrayList<>(); //勉强能过,但是linkedlist过不了
for (int i = 0; i < n; i++) list.add(i);
int idx = 0;
while (n > 1){
idx = (idx + m - 1) % n;
list.remove(idx);
n --;
}
return list.get(0);
而数学规律是:dp[i]=(dp[i−1]+m)%i 难
int x = 0;
for (int i = 2; i <= n; i++) {
x = (x + m) % i;
}
return x;
题目:不能用乘法除法,不能用for, while, if, else, switch, case等,不能用a?b:c
分析:本来就是一道递归问题,对于递归边界,需要判断,现在需要用一种运算替代判断,用&&替代,当前一个条件不满足,就不会进入后面的条件
public int sumNums(int n) {
boolean a = n > 1 && (n += sumNums(n - 1)) > 0;
return n;
}
题目:计算两数之和,不能用±*/
分析:也就是只能用位运算;联想到了数电中加法器如何实现,就是靠与或运算实现相加和进位
public int add(int a, int b) {
while(b != 0){
int temp = (a & b) << 1; //都1才进位
a = a ^ b; //异或运算,不同为1,相同为0
b = temp;
}
return a;
}