剑指Offer

先刷了一遍剑指Offer,许多题目用的是自己的“土办法”。这里总结了一下,结合自己的解法和题解中一些大佬的解法,形成了对一道题目的的分析,包括巧妙的数据结构,常用的算法思想,冷门的api以及固定的套路和牛逼的技巧。

目录

      • 1.数组
          • J03_[数组中重复的数字](https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/)
          • J04_[二维数组中的查找](https://leetcode-cn.com/problems/er-wei-shu-zu-zhong-de-cha-zhao-lcof/)
          • J12_[矩阵中的路径](https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/)
          • J13_[机器人的运行范围](https://leetcode-cn.com/problems/ji-qi-ren-de-yun-dong-fan-wei-lcof/)
          • J11_[旋转数组中的最小数字](https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/)
          • J21_[调整数组顺序使奇数位于偶数前面](https://leetcode-cn.com/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/)
          • J29_[顺时针打印矩阵](https://leetcode-cn.com/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/)
          • J39_[数组中出现次数超过一半的数字](https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/)
          • J40_[数据流中的中位数](https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof/)
          • J42_[连续子数组的最大和](https://leetcode-cn.com/problems/lian-xu-zi-shu-zu-de-zui-da-he-lcof/)
          • J47_[礼物的最大值](https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/)
          • J51_[数组中的逆序对](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/)
          • J53_I[在排序数组中查找数字](https://leetcode-cn.com/problems/zai-pai-xu-shu-zu-zhong-cha-zhao-shu-zi-lcof/)
          • J56_I[数组中数字出现的次数](https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-lcof/)
          • J56_II[数组中数字出现的次数II](https://leetcode-cn.com/problems/shu-zu-zhong-shu-zi-chu-xian-de-ci-shu-ii-lcof/)
          • J57_和为s[的两个数字](https://leetcode-cn.com/problems/he-wei-sde-liang-ge-shu-zi-lcof/)
          • J57_II[和为s的连续正数序列](https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/)
          • J59_I[滑动窗口的最大值](https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof/)
          • J61_[扑克牌种的顺子](https://leetcode-cn.com/problems/bu-ke-pai-zhong-de-shun-zi-lcof/)
          • J66_[构建乘积数组](https://leetcode-cn.com/problems/gou-jian-cheng-ji-shu-zu-lcof/)
      • 2.字符串
          • J05_[替换空格](https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/)
          • J20_[表示数值的字符串](https://leetcode-cn.com/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/)
          • J48_[最长不含重复字符的字符串](https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/)
          • J50_[第一个只出现一次的字符](https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof/)
          • J58_I[翻转单词顺序](https://leetcode-cn.com/problems/fan-zhuan-dan-ci-shun-xu-lcof/)
          • J58_II[左旋字符串](https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/)
          • J67_[把字符串转换成整数](https://leetcode-cn.com/problems/ba-zi-fu-chuan-zhuan-huan-cheng-zheng-shu-lcof/)
      • 3.链表
          • J06_[从尾到头打印链表](https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/)
          • J18_[删除链表的节点](https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof/)
          • J22_[链表中倒数第k个节点](https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/)
          • J25_[合并两个排序的链表](https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/)
          • J35_[复杂链表的复制](https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/)
          • J52_[两个链表的第一个公共节点](https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/)
      • 4.二叉树
          • J07_[重建二叉树](https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/)
          • J26_[树的子结构](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/)
          • J27_[二叉树的镜像](https://leetcode-cn.com/problems/er-cha-shu-de-jing-xiang-lcof/)
          • J28_[对称的二叉树](https://leetcode-cn.com/problems/dui-cheng-de-er-cha-shu-lcof/)
          • J32_[打印二叉树](https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/)
          • J33_[二叉搜索树的后续遍历序列](https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/)
          • J34_[二叉树中和为某一值的路径](https://leetcode-cn.com/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/)
          • J36_[二叉搜索树与双向链表](https://leetcode-cn.com/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof/)
          • J37_[序列化和反序列化二叉树](https://leetcode-cn.com/problems/xu-lie-hua-er-cha-shu-lcof/)
          • J54_[二叉搜索树的第k大节点](https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/)
          • J55_I[二叉树的深度](https://leetcode-cn.com/problems/er-cha-shu-de-shen-du-lcof/)
          • J55_II[平衡二叉树](https://leetcode-cn.com/problems/ping-heng-er-cha-shu-lcof/)
          • J68_II[二叉树的最近公共祖先](https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/)
      • 5.栈
          • J09_[用两个栈实现队列](https://leetcode-cn.com/problems/yong-liang-ge-zhan-shi-xian-dui-lie-lcof/)
          • J30_包含main[函数的栈](https://leetcode-cn.com/problems/bao-han-minhan-shu-de-zhan-lcof/)
          • J31_栈的压入、[弹出序列](https://leetcode-cn.com/problems/zhan-de-ya-ru-dan-chu-xu-lie-lcof/)
          • J63_[股票的最大利润](https://leetcode-cn.com/problems/gu-piao-de-zui-da-li-run-lcof/)
      • 6.队列
          • J59_II[队列的最大值](https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/)
      • 7.递归和动态规划
          • J10_I[斐波那契数列](https://leetcode-cn.com/problems/fei-bo-na-qi-shu-lie-lcof/)
          • J10_II[青蛙跳台阶问题](https://leetcode-cn.com/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/)
          • J14-I[剪绳子](https://leetcode-cn.com/problems/jian-sheng-zi-lcof/)
          • J14-II剪绳子
          • J17_[打印从1到最大的n位数](https://leetcode-cn.com/problems/da-yin-cong-1dao-zui-da-de-nwei-shu-lcof/)
          • J19_[正则表达式匹配](https://leetcode-cn.com/problems/zheng-ze-biao-da-shi-pi-pei-lcof/)
          • J38_[字符串的排列](https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/)
          • J60_[n个骰子的点数](https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/)
      • 8.位运算
          • J15_[二进制中1的个数](https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/)
          • J16_[数值的整数次方](https://leetcode-cn.com/problems/shu-zhi-de-zheng-shu-ci-fang-lcof/)
      • 9.堆
          • J40_[最小的k个数](https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/)
      • 10.数学找规律
          • J43_[1~n整数中1出现的次数](https://leetcode-cn.com/problems/1nzheng-shu-zhong-1chu-xian-de-ci-shu-lcof/)
          • J44_[数字序列中某一位的数字](https://leetcode-cn.com/problems/shu-zi-xu-lie-zhong-mou-yi-wei-de-shu-zi-lcof/)
          • J45_[把数组排成最小的数](https://leetcode-cn.com/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/)
          • J49_[丑数](https://leetcode-cn.com/problems/chou-shu-lcof/)
          • J62_[圆圈中最后剩下数字](https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/)
          • J64_[求1+2+...+n](https://leetcode-cn.com/problems/qiu-12n-lcof/)
          • J65_[不用加减乘除做加法](https://leetcode-cn.com/problems/bu-yong-jia-jian-cheng-chu-zuo-jia-fa-lcof/)

1.数组

J03_数组中重复的数字

题目:数字范围在0~n-1的长度为n的数组,某些数字重复,返回任意重复的元素

分析:

  1. 指定了数据范围,可以借鉴桶排序思想,将数值和下标相对应,不重复的数子经过交换可以位于对应的下标出,但是对于重复数字a,假设a1已经移到了对应下标,当移动a2时,就会发现已经被正确的下标的a1占住了,从而可以得到重复的某个数字。
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]的位置
    }
}
  1. 另外,也可以使用set去重。
if(set.contains(num)) return num;	//用set检查,遇到重复则返回
J04_二维数组中的查找

题目:行列均递增的二维数组,返回是否包含某个元素target

分析:

  1. 暴力遍历,也就忽视了递增的条件。直观的思维:从左上角开始,如果小于target,那么分两路向下和向右,如果大于,则向上或向左,然后dfs,可以再加标志位判断是否遍历到。具体优化:不会经过向上或者向左,因为加入存在一条路径,那么一定能从左上角只向下和向右到达
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);
    }
}
  1. 分两路的dfs类似走迷宫,思想简单,但是能不能让走迷宫的规则更简单?1中的分叉可以联想到二叉树,将原二维数组逆时针旋转 45° ,会形成以原二维数组右上角元素位根的搜索二叉树
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 --;
}
J12_矩阵中的路径

题目: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;
J13_机器人的运行范围

题目: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();}	

J11_旋转数组中的最小数字

题目:对于[3 4 5 1 2],输出最小值1,注意原数组是非递减

分析:

  1. 可以依次判断后面的值是不是大于前面,第一次出现小于的地方就是最小值,全程递增就是第一个元素
  2. 二分,跳出while循环通常满足左右界相等或刚刚满足>关系,nums[mid] > nums[left]说明一直在递增,则left = mid + 1,如果小于,说明已经出现了转折,则right = mid
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]);	//因为比的是左边,极易跳过左边值
  1. 反思:每次中间值和左边比,发现会漏情况留到最后判断,那么能不能和右边比,如果小于右边,说明r = mid,如果大于右边,则l = mid + 1,如果等于右边,l –
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];
J21_调整数组顺序使奇数位于偶数前面

题目:输入一个数组,要求使数组中的所有奇数位于所有偶数之前

分析:如果利用辅助数组,很直观;如果在原数组交换,则有两种思路:

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 ++;
}

总结:前后指针更易理解,快慢指针思路非常巧妙

J29_顺时针打印矩阵

题目:顺时针从外到内打印矩阵

分析:第一次的做法是用四个变量记录状态,依次完成左右——上下——右左——下上,但是状态其实边界判断是有点复杂的,对行列奇数和偶数的判断不同,在一个就是并不是所有矩阵都一定按上面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 --;
}
J39_数组中出现次数超过一半的数字

题目:有一个数字,重复次数超过了数组长度一半,求这个数字

分析:要是用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;
J40_数据流中的中位数

题目:设计一种数据结果,实现添加元素和求中位数两个功能

分析:一种思路是排序,然后选择

不好想,利用双队列,回过头想,中位数就是把数据分成了两组,一组大的,一组小的

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;
}
J42_连续子数组的最大和

题目:输入整形数组,输出最大连续子数组和

分析:容易想到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];
}
J47_礼物的最大值

题目: 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];	//为了省空间,也可以在原数组加
J51_数组中的逆序对

题目:数组中任意两个数,如果下标小的数值大,那么就构成逆序对,输出总的逆序对数

分析:两次遍历,是暴力解法,肯定要寻求更优法。很容易想到分治,如果把原数组分成两半,假如各自一半都求出组内有多少,那么一个元素来自左半边,一个元素来自右半边该怎么求?如果还是两轮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}
}
J53_I在排序数组中查找数字

题目:在排序数组中统计某个数字的出现次数

分析:线性扫描简单直接,如果二分,就要先找到该元素,然后左右统计次数。更好的是直接定位到左、右边界

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;
J56_I数组中数字出现的次数

题目:数组中有两个数只出现了一次,输出这两个数字

分析:没有好办法了就是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;      
}
J56_II数组中数字出现的次数II

题目:输入一个数组,只有一个数字出现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; 
}

另外,也可以寻找到位运算的表达式,用有限状态机思想,具体参考题目后面的解题大佬的分析

J57_和为s的两个数字

题目:排序数组,输出任意一对和为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 --;
}
J57_II和为s的连续正数序列

题目:正数按序列从小到大,使之累加和为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++;
    }
}
J59_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();	//输出窗口内最大值
}
J61_扑克牌种的顺子

题目:扑克牌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

J66_构建乘积数组

题目:返回成绩数组,要求对下标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;
}

2.字符串

J05_替换空格

题目:把字符串s中的每个空格替换成"%20,返回替换后的字符串

分析:

  1. 判断字符是否空格,在StringBuilder上添加
for (int i = 0; i < s.length(); i++) {
    if(s.charAt(i) == ' '){
        sb.append("%20");
    }else {
        sb.append(s.charAt(i));
    }
}
  1. 所谓原地址修改,这样不能新建数组,扩大原数组长度到3倍长度,从原数组后序遍历,从后往前填充到新数组,这样一定不会出现:新数组覆盖掉未遍历到的旧数组元素。
J20_表示数值的字符串

题目:输入是一个字符串,输出是是否表示数值 【】表示必有,[]表示可选

数值类型:[若干空格]【一个小数或整数】[一个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,这个处理相当巧妙!

J48_最长不含重复字符的字符串

题目:返回不包含重复字符的子字符串长度

分析:可以用一个长度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);
}
J50_第一个只出现一次的字符

题目:比如"abaccdeff",输出第一个只出现一次的字符,就是b

分析:首先肯定得扫描完才能判断,可以考虑用按插入顺序排序的的map来存

Map<Character, Boolean> dic = new LinkedHashMap<>();
for(char c : chs)	map.put(c, !map.containsKey(c));

如果不考虑顺序,那么可以先扫一遍存map,再扫一遍,看谁的value满足要求也行

J58_I翻转单词顺序

题目:去掉前后空格,字符串按空格切分后倒序输出,切分后的单个字符串顺序不变

分析:直接切分,然后从后到前拼即可

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去定位到头,然后输出......
}
J58_II左旋字符串

题目:在字符串第某个下标前的字符串移动到末尾

分析:关于旋转,一般先拼接,在操作很方便

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());
J67_把字符串转换成整数

题目:输入字符串,它可能以空格开头,需要找到第一个非空字符,并且可能该字符是±,要把这之后的数字组合起来,出现的非数组应该被忽略,其它情况返回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的,就会稍微麻烦一点。

3.链表

J06_从尾到头打印链表

题目:数组返回从链表尾部到头部序列

分析:

  1. 单纯考虑数值,利用,先进后出
  2. 递归(程序执行本身就是压栈弹出的过程)
private void rec(ListNode head) {
    if(head == null) return;
    rec(head.next);
    list.add(head.val);		//list是成员变量ArrayList
}
  1. 先真正反转链表,再依次拿出放数组
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[]
  1. 反转链表非递归写法
ListNode pre = null;
while(head != null){
    ListNode next = head.next;
    head.next = pre;
    pre = head;
    head = next;
}
return pre;
J18_删除链表的节点

题目:删除链表某个节点,返回删除后的链表头节点

分析:双指针

while(cur != null && cur.val != val){   	//1---->3---->4【删除】---->5
    pre = cur;	//最后一次pre = 3,如果没有就会一直到pre=5,cur = null跳出
    cur = cur.next;		//最后一次cur = 4
}
J22_链表中倒数第k个节点

题目:输出链表倒数第k个节点,从下标1开始

分析:最直观的想法是计数,但是,利用双指针思想,相隔k个,当后面指针指向null,前面指针就是输出

while(cur != null){
    if(n ++ >= k)	res = res.next;
    cur = cur.next;
}
J25_合并两个排序的链表

题目:合并两个排好序的链表

分析:主要就是对于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;
J35_复杂链表的复制

题目:普通的单向链表+每个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;
}
J52_两个链表的第一个公共节点

问题:输入两个链表,求第一个公共节点

分析:扫一遍第一个,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);
}

4.二叉树

J07_重建二叉树

题目:输入的前序遍历和中序遍历(不包含重复数字),返回重建二叉树

分析:

  1. 很明显,前序遍历的第一个元素是根节点,然后,在中序遍历中找到该元素位置,以左为左子树,以右为右子树;从前序遍历的根之后找到新数组,和中序遍历的数组形成新的子问题,重复该步骤。
//成员变量存前序遍历数组和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;                                         
}
J26_树的子结构

题目:判断树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);
}
J27_二叉树的镜像

题目:输出一个二叉树的镜像

分析::并不是只把根节点的左右孩子交换,而是孩子的孩子也需要交换,即交换是递归的

//类似两数交换
if(root == null)    return null;
TreeNode l = root.left;
root.left = mirrorTree(root.right);;
root.right = mirrorTree(l);
return root;
J28_对称的二叉树

题目:一颗二叉树是否对称

分析:从根节点,首先判断左右孩子是否相等,然后判断左.左 == 右.右,左.右 == 右.左,至此三层以内相等;然后在判断左.左.左 == 右.右.右 左.左.右 == 右.右.左

穷举显然不切实,如果总局限在根节点1个节点,发现除了一一列举,没办法。如果把判断过程进来用数学去描述,不难发现共同的点,即第四层要去除第一个左或者右之后,和第三层判断是一样的。所以,相似的逻辑就是递归执行逻辑,不同的地方就是递归的入口条件不同

rec(root, root);	//需要加递归的出口判断
if(a.val == v.val)	return rec(a.left, b.right) && rec(a.right, b.left);	//递归逻辑
J32_打印二叉树

题目:输出为一维数组——>输出为包含层信息的二维数组——>之字型打印

分析:难点在于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);
}
J33_二叉搜索树的后续遍历序列

题目:判断数组是否是搜索二叉树的后续遍历结果

分析:明显就是递归,以[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);
}
J34_二叉树中和为某一值的路径

题目:从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();
}
J36_二叉搜索树与双向链表

题目:输入一棵二叉搜索树,要求一个排好序的双向链表(在树上修改)

分析:二叉搜索树的性质是把树压扁就是排好序的,即中序遍历结果是有序的,因此这个题就是中序遍历的升级

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);
}
J37_序列化和反序列化二叉树

题目:实现两个函数,将树层序遍历输出,根据层序遍历结果建立二叉树

分析:该输出是包含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++;
}
J54_二叉搜索树的第k大节点

题目:找到二叉搜索树的第k大节点

分析:二叉搜索树的中序遍历是有序数组,所以从右孩子遍历,再根节点,再左孩子,就是从大到小,遍历,记录即可

void dfs(TreeNode root) {
    if(root == null) return;
    dfs(root.right);
    if(--k == 0) {
        res = root.val;
        return;
    }
    dfs(root.left);
}

另外,如何重建二叉搜索树?先排序,再递归建子数

J55_I二叉树的深度

题目:输出二叉树的最大深度

分析:只要有一个孩子不为null,就能+1,但是都不为null,就得两边都扫描,去比较谁的深度更大,因此递归

if(root == null)    return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
J55_II平衡二叉树

题目:一棵树左右子树高度差不超过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;	
}
J68_II二叉树的最近公共祖先

题目:返回两个节点的最近公共祖先

分析:根结点肯定是,这时候要看一左一右,就是根结点,两左或两右,就该递归对应的左或右

写代码如何直到是左还是右,可以写专门函数,判断两个结点相对关系,即是左子树元素还是右子数元素,但是存在大量重复;下面是一种不用重复判断的写法:

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.在根结点的右子树上,那么该递归会回到跟结点,开始右子树递归。总之,很巧!

5.栈

J09_用两个栈实现队列

题目:用两个栈实现一种队列的数据结构,从头部删除元素,从尾部添加元素

分析:

  1. 存在栈st1和st2,添加元素则st1入栈,若删除元素,就要把st1的元素依次出栈入栈st2
public int deleteHead() {
    if (st2.isEmpty()) {	//先判断st2,为空则判断st1,搬运
        while (!st1.isEmpty()) 
            st2.push(st1.pop());
    } 
    if (st2.isEmpty()) 
        return -1;  
    return st2.pop();
    }
}
J30_包含main函数的栈

题目:要求实现一个栈,栈有存有所有元素的最小值,包含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();
}
J31_栈的压入、弹出序列

题目:给一个序列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;
}
J63_股票的最大利润

题目:一个无序数组,下标大的减下标小的最大值是多少

分析:两次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());
}

其实栈的作用是保存最小值,也可以用变量替代,该变量保存扫描到当前的最小值

6.队列

J59_II队列的最大值

题目:实现一个队列,入队和出队和遵循先进先出,但是要求能返回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();
}

7.递归和动态规划

J10_I斐波那契数列

题目:求第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;
}
J10_II青蛙跳台阶问题

分析:有一系列基于斐波那契数列的变形,只需改边界条件即可。改题目:对于上到第i级台阶,假设已知dp[i-1]和dp[i-2],则dp[i-2]上2步和dp[i-1]上一步覆盖了所有可能情况。

J14-I剪绳子

题目:长为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];
J14-II剪绳子

题目:I中n范围为[2,58],II为[2,1000],也就是要对大数取余,即大数越界情况下的求余问题

分析:思路一样,但是对于大数(超过int,long)处理比较麻烦

  1. BigInteger类,静态方法valueOf用于赋值,multiply,mod和max用于乘法和取余和比较,intValue转为整数
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();
  1. 补充贪心对应的高次幂求余(可以二分加快求解速度) 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;
}
J17_打印从1到最大的n位数

题目:返会数组[1 2 3 … 10^n-1]

分析:

  1. 不考虑大数问题
return IntStream.range(1, (int)Math.pow(10, n)).toArray();
  1. 考虑大数问题就需要转成字符串处理 dfs
// 初始化
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); // 递归
    }
}
J19_正则表达式匹配

题目:'.'表示任意一个字符,'*'表示它前面的字符可以出现任意次(含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
            }
        }
    }
J38_字符串的排列

题目:对于字符串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…

J60_n个骰子的点数

题目:输出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问题,实质上只依赖前一刻状态,也就是可以转化为一维数组的表示,当然二维更清晰一点

8.位运算

J15_二进制中1的个数

题目:输入为int类型整数n,返回对应二进制表示1个个数

分析:标准的位运算

while(n != 0){
    res += (n & 1);
    n = n >>> 1;	//无符号右移
}
J16_数值的整数次方

题目:实现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;
}

9.堆

J40_最小的k个数

题目:无序数组求最小的k个数

分析:

  1. 排序,输出

  2. 快排求得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;	//可以返回分割点的最终下标,便于函数递归
}
  1. 优先队列

  2. 自己实现堆排序

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);	//把最大元素依次移到最后
}
  1. TreeMap

    利用TreeMap对key进行排序,只需map.lastEntry()就能取出key和value,其实本质类似优先队列

​ 6. 记数排序(桶排序)

补充:建堆和插入元素

建堆:从第一个非叶子节点开始,向下满足,直到根节点也满足

插入:插入的结点加到最后,对新插入的结点进行上移操作就行

删除:用最后值替代删除值,若大于原值,则上移,若小于原值,则下移

10.数学找规律

J43_1~n整数中1出现的次数

题目: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; 
}
J44_数字序列中某一位的数字

题目: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判断第几个
J45_把数组排成最小的数

题目:非负整数数组,要求拼起来,数值最小

分析:如[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[]
J49_丑数

题目: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++;
}
J62_圆圈中最后剩下数字

题目: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;
J64_求1+2+…+n

题目:不能用乘法除法,不能用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;
}
J65_不用加减乘除做加法

题目:计算两数之和,不能用±*/

分析:也就是只能用位运算;联想到了数电中加法器如何实现,就是靠与或运算实现相加和进位

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;
}

你可能感兴趣的:(剑指Offer)