剑指offer67题 :
二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
public class Solution { public boolean Find(int target, int [][] array) { if(array.length==0 || array[0].length==0)return false; //尽量都写成左闭右闭区间的风格,在一开始就减一,上下界都是能够达到的值 int row = array.length -1; int col = array[0].length -1; //这里从右上开始,左下也可以。 //(不能从左上开始,不然不知道移动的方向。更不能从任意位置开始) int i = row; int j =0; while(i>=0 && j<=col){//范围用>=和<=,这样配合左闭右闭区间 if(array[i][j]>target)--i;//【每次判断都能剔除一整行或一整列】 else if(array[i][j]<target)++j;//这里的else if 不能用else,因为上面的语句可能会影响array[i][j]的值(改变了i的值) else return true;//将==放在最后,因为这个情况的概率最小,这样效率更高 } return false; } } //时间复杂度O(col+row) //空间复杂度O(1)
替换空格
原版(给stringBuffer):
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
return str.toString().replace(" ","%20"); //一句搞定
public class Solution {
public String replaceSpace(StringBuffer str) { //str的类型是StringBuffer,最后要转换成String//一共两轮,第一轮是扫描得到space个数
int space=0;
int L1=str.length();//str需要length();数组一般用length
for (int i=0;i<L1;i++)
{
if (str.charAt(i)==' ')space++; //【str.charAt(i)】
}
int L2=L1+2*space;
str.setLength(L2); //【str.setLength(L2)】一定要修改(加长)str的长度
L1--;L2--; //一定要一次性减1,来对齐数组下标
while (L1>=0&&L2>L1){
if (str.charAt(L1)!=' '){
str.setCharAt(L2--,str.charAt(L1)); //【str.setCharAt(下标,值)】
}
else{
str.setCharAt(L2--,'0');
str.setCharAt(L2--,'2');
str.setCharAt(L2--,'%');
}
L1--;
}
return str.toString(); //【str.toString()】
}
}
//时间复杂度:O(N)
//空间复杂度:O(space) =>直接在原来的StringBuffer上面改
新版(给String):
import java.util.*; public class Solution { public String replaceSpace (String s) { StringBuilder res = new StringBuilder(); int len =s.length()-1; for(int i=0;i<=len;++i){ if(s.charAt(i)!=' '){ //单引号代表char, 双引号代表String res.append(s.charAt(i)); //String可用charAt() } else res.append("%20");//StringBuilder的append可以是几乎所有类型 } return res.toString(); } } //时间:O(N) //空间:O(N)
从尾到头打印链表
输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
1)递归方法(系统栈)
import java.util.ArrayList; public class Solution { ArrayList<Integer> res = new ArrayList<Integer>(); //一定要在函数之前定义 public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { if(listNode!=null){ printListFromTailToHead(listNode.next); //没有用到printListFromTailToHead的返回值 res.add(listNode.val); //这个在递归后面,则可以做到倒序;如果在递归前就是正序 } return res; } }//空间O(N) 时间O(N)
2)自己写栈
import java.util.ArrayList; import java.util.Stack; public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer> res = new ArrayList<Integer>(); Stack<Integer> stack = new Stack<Integer>(); //posh + pop 搞定 while(listNode != null){ stack.push(listNode.val); listNode = listNode.next; } while(!stack.isEmpty()){ res.add(stack.pop()); } return res; } }//空间O(N) 时间O(N)
3)头插法(时间效率最低,所以说ArrayList尽量不要用头插法)
import java.util.ArrayList; public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer>mylist=new ArrayList<>();//含有<>和();别忘了new while(listNode!=null){//直接用null对应listNode就行 mylist.add(0,listNode.val);//list.add(0,value)在list的头部插入值,单次耗时O(N) listNode=listNode.next;//Java这样就不用到->指针了,只会用到STL里面定义过的操作 } return mylist; } }//空间O(N) 时间O(N^2)
重建二叉树
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
/** * Definition for binary tree * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ import java.util.Arrays;//Arrays.copyOfRange(,,); //针对pre[]和in[]数组,功能:选择范围复制==>左闭右开区间 public class Solution { public TreeNode reConstructBinaryTree(int [] pre,int [] in) { //返回的是根节点 if(pre.length==0||in.length==0)return null;//【递归的终结】 //也可以简化为 pre.length==0 (只判断一个即可) //不能写成 pre==null,因为pre==[]时,数组不是null但长度为零 TreeNode node=new TreeNode(pre[0]);//先序的第一个pre[0]永远是根节点,也是分左右子树的关键 for(int i=0;i<pre.length;i++){ //pre和in的子数组永远是对应相同长度的 if(pre[0]==in[i]){//每一次for循环,只有一次会执行if里面的语句 node.left=reConstructBinaryTree(Arrays.copyOfRange(pre,1,i+1),Arrays.copyOfRange(in,0,i)); node.right=reConstructBinaryTree(Arrays.copyOfRange(pre,i+1,pre.length),Arrays.copyOfRange(in,i+1,in.length)); }//在建设node.val后,再递归调用获取node.left和node.right,这样3步后,一个node就完整建立起来了 } return node; } } //复杂度方面:最坏情况下(树是一条直线)每一层递归从O(n)直到O(1),因为每一层都会至少减少1个复制的。
//最坏情况下,N层递归,此时时间空间复杂度都是O(N^2) //平均情况下,log(N)层递归,此时时间空间复杂度都是O(NlogN)
旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
1)O(n) 暴力扫描
import java.util.ArrayList; public class Solution { public int minNumberInRotateArray(int [] array) { if (array.length==0)return 0; int min=array[0]; for (int i=0;i<array.length;i++){ if (array[i]<min)min=array[i]; } return min; } } //粗暴查找,时间O(N)
2)二分法
import java.util.ArrayList; public class Solution { public int minNumberInRotateArray(int [] array) { int len=array.length; if(len==0)return 0; int left=0;int right=len-1;//自己写区间的时候,尽量用“左闭右闭”区间,不然容易出错。(不要用左闭右开区间!!) while(left<right){ if(array[left]<array[right]){//严格小于//说明区间里面没有“断层”,单调增/单调不减 return array[left]; } int mid=(left+right)/2;//左右区间内有“断层”的时候,需要探测mid位置的值 //3种情况:大于小于等于(最好画个图)//最好是mid和right来比较;选mid和left来比较时,还需要再次分类判断(因为只有2个数时,mid和left重合) if(array[mid]>array[right])left=mid+1; else if(array[mid]<array[right])right=mid; else if(array[mid]==array[right])--right;//这种情况容易考虑不到,导致区间无法收敛 } return array[right]; //此时只有一个元素,所以left==right } } //二分查找,平均时间O(logN) //最坏情况(全部相等)时间复杂度O(N)
用两个栈实现队列
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
import java.util.Stack; public class Solution { Stack<Integer> stack1 = new Stack<Integer>(); Stack<Integer> stack2 = new Stack<Integer>(); public void push(int node) { stack1.push(node); } public int pop() { if(stack2.isEmpty()){ //【只有stack2排空了,才会由1到2,且一次性全部】 while(!stack1.isEmpty())stack2.push(stack1.pop()); } return stack2.pop();//(无论上面的if语句结果怎样,这里都会pop出一个) } }
斐波那契数列
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39
public class Solution { public int Fibonacci(int n) { int[] fi=new int[40];//设置数组记录中间结果,不然重复计算太多 fi[0]=0;fi[1]=1; for(int i=2;i<=n;i++){ fi[i]=fi[i-1]+fi[i-2];//(一开始错误是把这里面的i写成n了) } return fi[n]; } } //动态规划,时间复杂度O(N),空间复杂度O(N) //如果用递归,时间复杂度O(1.618^N)【上网查的,略小于2^N】,空间复杂度O(1)【不包括系统栈空间】
跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
1)斐波拉切-O(N)动态规划
public class Solution { public int JumpFloor(int target) { int frog[]=new int[100]; frog[1]=1;frog[2]=2; for (int i=3;i<=target;i++){ frog[i]=frog[i-1]+frog[i-2]; } return frog[target]; } } //原理同:斐波那契数列 //【动态规划】时间O(N),空间O(N) //如果只要最后的结果,那么可以撤销数组,使用a/b/c三个变量存储即可。空间复杂度减为O(1)
2)空间O(1)的方法
public class Solution { public int jumpFloor(int target) { if(target<=2)return target; int lastOne = 2; //现在位置上一个,相当于fi[i-1] int lastTwo = 1; //相当于fi[i-2] int res = 0; for(int i=3; i<=target; ++i){ res = lastOne + lastTwo; lastTwo = lastOne; lastOne = res; } return res; } } //这种方法的空间复杂度为:O(1) //时间复杂度虽然也为O(N),但是比上一种动态规划的方法耗时,因为循环里面操作较多 //相当于时间换空间,花费时间在不断倒腾地方
变态跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
1)找出公式
public class Solution { public int JumpFloorII(int target) { int way=1;for(int i=1;i<target;i++)way*=2;return way; } } //【找出数学公式】2的n-1次方:类似向n个点之间的n-1个空画横线 // 其实不难找,在找递推公式时,前几项一写就知道了 // 时间 O(N) // 空间 O(1)
2)(动态规划)硬算
public class Solution { public int jumpFloorII(int target) { int[] array =new int[100]; array[1] = 1; for(int i=2; i<=target; ++i){ int sum = 0; for(int j=1; j<=i-1; ++j)sum+=array[j]; array[i] = sum +1; //之前所有路径,再加上直接全部的1个跳法 } return array[target]; } } //时间 O(N^2) //空间 O(N)
矩形覆盖
public class Solution { public int rectCover(int target) { int fi[] = new int[100]; for(int i= 0; i<=2; ++i)fi[i]=i; for(int i=3; i<=target; ++i)fi[i]=fi[i-1]+fi[i-2]; return fi[target]; } } //(除了初始少许不一样,后面是斐波拉切) // 找递推关系:分解情况==》最右边只可能为竖或横两种情况,这两种情况无交集,分别占用1个块块和2个块块
数值的整数次方
public class Solution { public double Power(double base, int exponent) { double res = 1.0; int n = 0; if(exponent<0) n = exponent * (-1); else n = exponent; for(int i=1;i<=n;++i){ res*=base; } if(exponent<0)res=1/res; return res; } } //时间 O(exponent) //空间 O(1)
求1+2+3+...+n
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
方法一:Math.pow()
public class Solution { public int Sum_Solution(int n) { return (int)(Math.pow(n,2)+n)>>1; } }//时间O(1) 空间O(1)
方法二:短路递归
public class Solution {// 1.需要重复结构->使用递归 2.需要判断->使用&&特性 int sum = 0; public int Sum_Solution(int n) { boolean bool = ((n>=1) && (sum += (n + Sum_Solution(n-1)))>0); return sum; } }//时间O(N) 系统栈空间O(N)
连续子数组的最大和
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为 O(n).
//这是实用算法课上讲过的方法 public class Solution { public int FindGreatestSumOfSubArray(int[] array) { if(array.length ==0)return 0; int max = Integer.MIN_VALUE;//全局最大值 int currentSum = 0;//邻近最大值:小于0时候熔断,最小为0 for(int i =0; i<=array.length-1; ++i){ currentSum += array[i]; if(currentSum > max) max=currentSum; if(currentSum<0)currentSum=0; } return max; } } //时间 O(N) //空间 O(1)
数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
方法一:基础版
public class Solution { public int MoreThanHalfNum_Solution(int [] array) { int len=array.length; int target=1+len/2; for(int i=0;i<len;i++){ int temp=array[i]; int num=target; for (int j=0;j<len;j++){ if (array[j]==temp)num--; } if (num<=0)return temp; } return 0; } } //基础方法:时间复杂度O(N^2),空间复杂度O(1)
方法二:终极进阶版(最优解)
//候选法,两步走:1.找候选 2.验证 //候选不一定是过半,但过半一定进候选 public class Solution { public int MoreThanHalfNum_Solution(int [] array) { //1.找候选: int power = 0;//能量值,一般不等于出现次数,因为会互相抵消 int candidate = 0; for(int i=0; i<=array.length-1; ++i){ if(power == 0)candidate = array[i]; if(candidate == array[i]) ++power; else --power; } //2.验证: int count =0;//候选者的次数,不同于power for(int i=0; i<=array.length-1; ++i){ if(candidate == array[i]) ++count; } if(count>array.length/2)return candidate; return 0; } } //两个O(N)的for循环,所以时间复杂度:O(N) //空间复杂度:O(1)
调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
import java.util.*; public class Solution { public int[] reOrderArray (int[] array) { int len = array.length; //1个数组 + 1个下标: int [] res = new int[len]; int k =0; for(int i=0; i<=len-1; ++i){ if(array[i]%2 == 1) res[k++] = array[i]; } for(int i=0; i<=len-1; ++i){ if(array[i]%2 ==0) res[k++] = array[i]; } return res; } } //在保证奇数偶数内部相对顺序的情况下,这种方法就是最优了,时间、空间都是O(n) //书上的题目要求仅仅是将奇偶数分开,那么用类似快排的two pointer就行,时间O(n),空间是O(1)
丑数
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
方法一:原始版
import java.util.Arrays; public class Solution { public int GetUglyNumber_Solution(int index) { if (index<=0)return 0; int array[]=new int[2000]; array[0]=1; for (int i=1;i<index;i++){ int bound=array[i-1];//现存的最大值 int m2=1;int m3=1;int m5=1; for(int k=0;;k++){ if (2*array[k]>bound){m2=2*array[k];break;} //计算array[i]的候选值。//不过重复计算太多了,每次重新计算又不去记录 } for(int k=0;;k++){ if (3*array[k]>bound){m3=3*array[k];break;} } for(int k=0;;k++){ if (5*array[k]>bound){m5=5*array[k];break;} } int min=m2; if (m3<min)min=m3; if (m5<min)min=m5; array[i]=min; //获取大于array[i-1]的最小值 } return array[index-1]; } } //时间复杂度:略大于或等于O(NlogN),难以计算具体值 //空间复杂度:O(N)
强化升级版(最优解)
import java.lang.Math; public class Solution { public int GetUglyNumber_Solution(int index) { int ugly [] = new int [2000]; ugly[1] = 1;//第一个丑数是1 //ugly[]数组:从1开始,而不是0,增加可读性 int t2 = 1; int t3 = 1; int t5 = 1;//标记2/3/5这三个赛道中(非独立),潜在候选者的位置 //ugly[]下标 //t2t3t5跑得比i要慢 for(int i=2; i<=index; ++i){ ugly[i] = Math.min(Math.min(2*ugly[t2],3*ugly[t3]),5*ugly[t5]);//Java里面的min()太low了,只能两个数 if(ugly[i] == 2*ugly[t2]) ++t2;//t2沿着主干线ugly[]走到下一个:因为这个被选中了,选下一个为候选 if(ugly[i] == 3*ugly[t3]) ++t3; if(ugly[i] == 5*ugly[t5]) ++t5;//为什么要搞三个类似语句?因为这三个可能中一个、也可能中两个、或三个全中(三个因子都含有) } return ugly[index]; } } //时间 O(N) 空间 O(N)
最小的K个数 (Top-K问题)
输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
方法一:插入排序改装版
import java.util.ArrayList; public class Solution { public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) { ArrayList<Integer> res = new ArrayList<Integer>(); if(k<=0 || k>input.length)return res; for(int i=0; i<=k-1; ++i){ int min = Integer.MAX_VALUE; int index = 0;//标记这一轮的min的位置 for(int j=0; j<=input.length-1; ++j){ if(input[j] < min){ min = input[j]; index = j; } } res.add(min); input[index] = Integer.MAX_VALUE;//将此轮min给移除 } return res; } } //时间复杂度:O(N*k) //空间复杂度:O(k)
方法二:基于Partition,会在原始数组上修改 时间O(N)
import java.util.ArrayList; public class Solution { public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) { ArrayList<Integer> res = new ArrayList<Integer>(); if(k<=0 || k>input.length)return res; int divide = 0; int left =0; int right =input.length-1; while(true){ divide = partition(input, left, right); if(divide > k-1){ right=divide-1; } else if(divide < k-1){ left = divide+1; } else if(divide == k-1)break;//到位 } for(int i=0; i<=k-1; ++i){ res.add(input[i]); } return res; } public int partition(int [] input, int left, int right){ int temp = input[left]; while(left<right){ while(left<right && input[right]>=temp)--right;//注意这里的大小判断中,有等号 input[left] = input[right];//覆盖的时候,不需要移动下标 while(left<right && input[left]<=temp)++left; input[right] = input[left]; } input[left] = temp; return left;//此时left==right } } //时间复杂度分析:partition会遍历left~right; //平均情况下:O(N)+O(N/2)+...+O(1)=O(2N)=O(N) //最坏情况下:O(N)+O(N-1)+...+O(1)=O(N*(N-1)/2)=O(N^2) //时间:O(N) //空间:O(k)
方法三:堆排序,适合处理海量数据 时间O(N*logK)
//堆排序适合海量流数据,因为在运算的时候,允许后去不断输入、而不会破坏前面已经计算的结果 //使用Java的PriorityQueue基于堆;堆的规模是k,而不是N
//add/remove/poll操作的时间复杂度都是O(logK) ,peek复杂度是O(1)
//top-k问题:求最大用小根堆;求最小用大根堆。
import java.util.*;//全靠util了: ArrayList/PriorityQueue/Collections public class Solution { public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) { ArrayList<Integer> result=new ArrayList<Integer>(); if(k<=0||input.length<k)return result; PriorityQueue<Integer> MaxPeekHeap=new PriorityQueue<Integer>(k,Collections.reverseOrder());//堆初始化的时候必须要有参数(堆大小:大根/小根,默认小根堆) for(int i=0;i<input.length;i++){ if(MaxPeekHeap.size()<k){//开始为空的时候直接入堆 MaxPeekHeap.add(input[i]); } else{ if(MaxPeekHeap.peek()>input[i]){//大顶堆的peek是堆中的最大值,如果input[i]比堆中最大值大,就直接忽略 MaxPeekHeap.remove(MaxPeekHeap.peek()); MaxPeekHeap.add(input[i]);//O(logK) } } } while(!MaxPeekHeap.isEmpty()){ result.add(MaxPeekHeap.poll());//PriorityQueue里面用了:add/remove/peek/poll } return result; } } //时间复杂度O(Nlogk) //空间复杂度O(k)
顺时针打印矩阵 (旋转矩阵)
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
import java.util.ArrayList; public class Solution { public ArrayList<Integer> printMatrix(int [][] matrix) { ArrayList<Integer>res = new ArrayList<Integer>(); //定义上下左右的边界,共需要4个变量 int up = 0; int down = matrix.length -1; int left = 0; int right = matrix[0].length -1; while(true){//while中有4个类似的结构,但是不适合合并,因为合并也不会减少代码 if(left>right)break;//函数开头不用另外验matrix,因为while里面已经做得很完善了 for(int i=left; i<=right; ++i){ res.add(matrix[up][i]); } ++up;//缩小边界 if(up>down)break; for(int i=up; i<=down; ++i){ res.add(matrix[i][right]); } --right; if(left>right)break; for(int i=right; i>=left; --i){ res.add(matrix[down][i]); } --down; if(up>down)break; for(int i=down; i>=up; --i){ res.add(matrix[i][left]); } ++left; } return res; } }
二进制中1的个数
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
方法一:按位与
public class Solution { public int NumberOf1(int n) {//可以直接拿int类型,当做二进制来进行位运算 int count =0; int mark = 0x01; for(int i=0;i<32;++i){//从最低位到最高位,一个个试 //或者用while(mark !=0)也可以 if((n & mark)!=0)++count;//不能是if(bit&n!=0),少了括号后,先计算n!=0(判断优先于按位运算) mark<<=1;//mark中唯一的1,左移一位 } return count; } } //时间复杂度O(1)==>O(32) //空间复杂度O(1) //C++要3ms,Java要13ms
方法二:奇技淫巧法(由于规模限制,所以并无明显优势)
public class Solution { public int NumberOf1(int n) { int count=0; while(n!=0){ n=n&(n-1);//神奇的方法:补码&反码 //&的含义是只有两个都是1,结果才是1 count++;//跳过了补码中没有1的位,每一轮循环弹无虚发,都找到一个1 } return count; } } //时间复杂度O(1)==>O(16),输入n的补码中1的个数,平均为O(16) //空间复杂度O(1) //C++要3ms,Java要13ms //和上面一样时间,白优化了hhh
数字在排序数组中出现的次数
统计一个数字在排序数组中出现的次数。
方法一:基础版 极端情况为O(N)
//先找到任意一个,然后再顺藤摸瓜 //绝大多数大数据的情况下,都是O(logN) //极端情况,要找的那个重复数量奇多无比O(N) public class Solution { public int GetNumberOfK(int [] array , int k) { if(array.length==0)return 0; int left=0; int right=array.length-1; int index=0; int flag=0; while (left<=right){ int mid=(left+right)/2; if (array[mid]==k){ index=mid;flag=1;break; } else if(array[mid]>k)right=mid-1; else if(array[mid]<k)left=mid+1; } if (flag==0)return 0; while (index-1>=0&&array[index-1]==k)index--;//移到最左边 int count=0; while (index<array.length&&array[index++]==k)count++;//从左到右统计 return count; } }
方法二:进阶版 稳定复杂度为O(logN)
//使用两个二分查找O(logN),找到目标数字的上下界 public class Solution { public int GetNumberOfK(int [] array , int k) { int left=0;int right=array.length;//用于夹紧的左右范围 //右边为数组结束的后一个,不然右边界无法到达这里(见下方) while(left<right){//一直夹到left==right为止,所以可以用单方向的array[mid]<k int mid=(left+right)/2;//mid=left+(right-left)/2 if(array[mid]<k){//左边界(找的是目标中最左边一个) left=mid+1;//注意是左边加一,不然最后容易夹不紧 } else right=mid; } int k1=left;//目标左边界 left=0;right=array.length;//重置 while(left<right){ int mid=(left+right)/2; if(array[mid]<=k){//右边界(找的是第一个开始不是目标的位置) //与上面几乎对称,只是将<改为<= left=mid+1; } else right=mid; } int k2=left;//目标右边界 return k2-k1;//可能为0 //如果寻找的值不存在,则由于没有等于这个情况,上下两个while输出的位置一致 } } //时间复杂度:O(logN) //空间复杂度O(1)
再简化:
public class Solution { public int GetNumberOfK(int [] array , int k) { return find(array, k) - find(array, k-1); } public int find(int [] array , int k){//此方法找右边界 int left = 0; int right = array.length; while(left<right){ int mid = (left+right)/2; if(array[mid] <= k){ left = mid+1; } else right = mid; } return left;//left==right } }
链表中倒数最后k个结点
public class Solution { public ListNode FindKthToTail (ListNode pHead, int k) { if(pHead==null || k<=0)return null; ListNode pre = pHead;//head是第一个节点 ListNode res = pHead; for(int i=1; i<=k; ++i){ if(pre!= null)//一定要搞清楚条件 pre=pre.next; else return null;//倒数第k的k值大于链表总长度 } while(pre!=null){ pre=pre.next; res=res.next; } return res; } } //时间 O(N) //空间 O(1)
2)朴素方法
public class Solution { public ListNode FindKthToTail (ListNode pHead, int k) { int len =0; ListNode p = pHead; while(p != null){ p = p.next; ++len; } if(len<k)return null; p = pHead; for(int i=1; i<=len-k; ++i){ p=p.next; } return p; } } //【个人认为】前后指针的方法,其实和朴素的方法没有实质的性能差别。//感觉只能炫技而已 //时间上:两种方法的遍历都是 一个指针完整遍历N跳,另一个/另一次遍历N-k跳; //空间上:朴素方法只要一个节点指针,前后节点法占用两个节点指针; //总的来说,就是【一个指针遍历两次,和两个指针遍历一次】这样子,性能上,并没有两倍的时间差别
反转链表
输入一个链表,反转链表后,输出新链表的表头。
public class Solution { public ListNode ReverseList(ListNode head) { //【设置3个指针三连排:p1/p2/p3】 //反转时候最怕的就是单链断掉,所以这里用几个节点指针来缓存“断链操作”处的信息 ListNode p1=null;//第一次使head指向null ListNode p2=head;//p2是head ListNode p3=null; while(p2!=null){ p3=p2.next;//p3的作用是在p2.next被翻转前,标记原始的p2.next p2.next=p1;//反转操作 p1=p2; p2=p3; } return p1;//最后p1为翻转后的head } } //时间复杂度O(N),空间复杂度O(1)
合并两个排序的链表
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
1)朴素方法
public class Solution { public ListNode Merge(ListNode list1,ListNode list2) { //没有虚拟头结点的坏处:要额外拎出来头部的过程,而不是统一 if(list1 == null)return list2; if(list2 == null)return list1; ListNode p0 = null;//标记头部,不能动 if(list1.val < list2.val){ p0=list1; list1=list1.next; } else{ p0=list2; list2=list2.next; } ListNode p=p0; while(list1 != null && list2 != null){//正式主体过程 if(list1.val < list2.val){ p.next=list1; p=p.next;//【千万别漏了这个】 list1=list1.next; } else{ p.next=list2; p=p.next; list2=list2.next;//直接修改题目变量,可节约空间 } } if(list1==null)p.next=list2; if(list2==null)p.next=list1; return p0; } } // 时间 O(N), 空间 O(1)
2)虚拟头结点:统一过程&精简代码
public class Solution { public ListNode Merge(ListNode list1,ListNode list2) { //建立虚拟头结点,可以统一过程&精简代码,同时减少或不用考虑头部的情况 //此代码和上面相比,只有函数里的第一行和最后一行是修改的 ListNode p0 = new ListNode(-2147483648);//建立head前面的虚拟头结点 //建立的时候不能用null,必须实例化 ListNode p=p0; while(list1 != null && list2 != null){ if(list1.val < list2.val){ p.next=list1; p=p.next; list1=list1.next; } else{ p.next=list2; p=p.next; list2=list2.next; } } if(list1==null)p.next=list2; if(list2==null)p.next=list1; return p0.next;//虚拟头结点一直在那不动,返回它的下一个即为第一个真实节点 } }
二叉树的镜像
操作给定的二叉树,将其变换为源二叉树的镜像。
public class Solution { public TreeNode Mirror (TreeNode pRoot) { if(pRoot!=null){//递归终止条件:null时不再向下 TreeNode temp = pRoot.left; pRoot.left = pRoot.right; pRoot.right = temp;//经典的三步交换 Mirror(pRoot.left); Mirror(pRoot.right);//向下传递左右子树根节点,向上无传递 } return pRoot; } } //时间O(N) //空间不是O(1)而是O(N)==>递归占用N层系统栈
二叉树的深度
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
public class Solution { public int TreeDepth(TreeNode root) {//这个题目的深度depth==层数layer //有的定义depth==layer-1 if(root != null){ int left = TreeDepth(root.left); int right = TreeDepth(root.right); return left>right ? left+1:right+1; } return 0;//root=null时,返回-1 //此时的root是代称,而不一定是整个树的root } }//所有节点都要走一遍:时间O(N) //后续遍历,栈空间无法节约==>空间O(N)
数组中只出现一次的数字
一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
方法一:基础HashMap方法(此题用这个效率不高)
//如果针对本题的特殊条件,那么这个方法并没有利用好全部条件 //如果是出现2/3/4/5/...次,本方法都适用; import java.util.*; public class Solution { public int[] FindNumsAppearOnce (int[] array) { int[] res = new int[2]; HashMap<Integer, Integer> num = new HashMap<Integer, Integer>(); //【HashMap<Key,Value>】 //这里的key是数组里的数字,value是出现次数 for(int i=0; i<=array.length-1; ++i){ if(! num.containsKey(array[i])){//【containsKey(Key)】 //其实STL里面还有containsValue(Value) num.put(array[i],0);//map不提供初始化(需要自己初始化) } int oldValue = num.get(array[i]);//【get(Key)】 num.put(array[i],oldValue+1);//【put(Key,Value)】//put直接更新值,不需要先删旧值 } int k=0; for(int i=0; i<=array.length-1; ++i){ if(num.get(array[i])==1)res[k++]=array[i]; } if(res[0]>res[1]){ int temp = res[1]; res[1] = res[0]; res[0] = temp; } return res; } } //空间O(N) //时间:大于O(N) , 需要考虑到HashMap中各个操作的复杂度
方法二:位运算(最优解)
import java.util.*; public class Solution { public int[] FindNumsAppearOnce (int[] array) { int xor = 0; for(int i=0; i<=array.length-1; ++i){ xor ^= array[i]; } int bit = 0x01;//用于探测 //写int bit=1也行 while((xor & bit) == 0)bit <<= 1;//找到一个xor为1的位 //1说明a与b在这位不同,所以异或为1 //与运算& 异或运算^ ...按位运算的优先级低于==,所以要加括号 int a = 0; int b = 0; for(int i=0; i<=array.length-1; ++i){//把array[i]分成两类 if((bit & array[i]) == 0) a ^= array[i];//array[i]的judge位为0 if((bit & array[i]) != 0) b ^= array[i]; } if(a < b) return new int [] {a,b}; else return new int [] {b,a}; } }//时间O(N) 空间O(1)
数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
方法一:排序寻找法(最优)
public class Solution { //【题目特殊条件】一个长度为n的数组里的所有数字都在0到n-1的范围内。 public static int duplicate(int numbers[]) { for (int i=0;i< numbers.length;i++){ while(numbers[i]!=i){//这里虽然是while,看似是双重循环,但实际上每次交换都会有一个数字到自己的最终排序位置上 =>时间复杂度不是O(n^2),而是O(n) if (numbers[i]==numbers[numbers[i]]){ return numbers[i]; } else{//这里进行排序,但不是普通的排序,而是特殊条件下的排序,所以只需要O(n),而不是O(nlogn) int temp=numbers[numbers[i]];//【这种嵌套的模式来交换,不是普通的temp三行交换,中间可能会出现干扰 =>左边的值被覆盖】 numbers[numbers[i]]=numbers[i];//建议先改numbers[numbers[i]],再改numbers[i] numbers[i]=temp;//注意左边用的量不要被覆盖 } } } return -1;//一直没找到,才会到这行 } } //时间复杂度:O(N) //空间复杂度:O(1)
方法二:HashSet
此方法没有利用题目的特性,时间+空间效率,都比方法一要低;
但优点是:1. 不破坏原有数组 2.比较通用,容易想到 3.代码简单,又快又不容易错
import java.util.*; public class Solution { public int duplicate (int[] numbers) { HashSet<Integer> set = new HashSet<Integer>(); for(int i=0; i<=numbers.length-1; ++i){ if(set.contains(numbers[i]))return numbers[i]; else set.add(numbers[i]); //利用set的STL性质(add时有重复返回false)可以优化效率,将上面两句替换为: //if(!set.add(numbers[i]))return numbers[i]; } return -1; } } //时间: HashSet/HashMap比较复杂,要根据N的规模分类讨论 //空间 O(N)
删除链表中重复的结点:较难
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
//注意:不是去重。本题重复的节点不保留 public class Solution { public ListNode deleteDuplication(ListNode pHead) { ListNode vhead=new ListNode(0);//虚拟头结点 //vhead.next=pHead; ListNode pre=vhead;//pre用于接收无重复的节点 ListNode cur=pHead;//cur用于遍历原来的链 //if()return null; while(cur!=null){ if(cur.next!=null && cur.val==cur.next.val){//这个if-else的大结构里面,如果有相等就判死刑 //&&左边是&&右边的保障 while(cur.next!=null && cur.val==cur.next.val){//同上 //因为有序的链表,所以只要相邻的判断就可知道有没有重复 cur=cur.next; } cur=cur.next;//cur指向下一种数字的第一个 } else{//接收节点:无重复的节点 pre.next=cur; pre=pre.next; cur=cur.next; } } pre.next=null;//最后指向null收尾 //不然每次接收都是后面一串,可能会多一些重复的尾巴 return vhead.next; } } //单链表扫一遍即完成任务
//时间O(N) 空间O(1)
包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。
import java.util.Stack; public class Solution { Stack<Integer>stack = new Stack<Integer>();//【基础栈】用于完成push、pop、top这些基础功能 Stack<Integer>min = new Stack<Integer>();//【辅助栈】专门用于记录最小值序列 //不能只用一个int,因为这样只有全局最小值 //例子: // stack:342053 // min: 332000 public void push(int node) { stack.push(node); if(min.isEmpty() || node<min.peek())//栈操作前要先判断isEmpty() min.push(node); else min.push(min.peek()); } public void pop() { stack.pop(); min.pop();//push、pop的时候,两个stack同步==>两个栈的长度始终是保持一致 } public int top() { return stack.peek();//Stack-STL中,peek()的意思是:不改变栈,观察栈顶的值 } public int min() { return min.peek(); } } //空间换时间:辅助栈O(N)的空间,使得查找min的操作时间从O(N)降为O(1),从而满足题目的要求。
两个链表的第一个公共结点
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
方法一:朴素对齐法(并列最优)
//本题朴素方法:代码虽长,但易读易懂、效率也同样高 public class Solution { public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { if (pHead1==null||pHead2==null)return null; int len1=0;//测长度 int len2=0; ListNode p1=pHead1; ListNode p2=pHead2; while (p1!=null){ len1++; p1=p1.next; } while(p2!=null){ len2++; p2=p2.next; } int diff=0; p1=pHead1;p2=pHead2;//重新初始化 if (len1>len2){ diff=len1-len2; for (int i=0;i<diff;i++)p1=p1.next;//长的先跑diff,用于对齐 } else { diff=len2-len1; for (int i=0;i<diff;i++)p2=p2.next; } while(p1!=null && p2!=null){ if (p1.val==p2.val) return p1; p1=p1.next; p2=p2.next; } return null; } } //时间复杂度:遍历2*(m+n)-2*common个节点 => O(N) //编程哲理:首要是【功能+性能】;性能相同的情况下:方便自己写、方便别人读;//如果要炫技:要的是效果,而不是炫技本身 @本题的双指针法
方法二:双指针法(性能一致,优点是短)
public class Solution { public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) { if(pHead1 == null || pHead2 == null)return null; ListNode p1 = pHead1; ListNode p2 = pHead2; while(p1 != p2){ p1=p1.next; p2=p2.next; if(p1 == null && p2 ==null)return null; if(p1 == null)p1 = pHead2;//这两行是关键:如果到头了,就从另一个的开头“复活” if(p2 == null)p2 = pHead1; } return p1; } } //时间复杂度:遍历2*(m+n)-2*common个节点 //时间O(m+n) //空间O(1)
整数中1出现的次数(从1到n整数中1出现的次数)
求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
方法一:朴素方法
public class Solution { public int NumberOf1Between1AndN_Solution(int n) { int count=0; for(int i=1;i<=n;i++){ int j=i; while (j!=0){ if(j%10==1)count++; j=j/10; } } return count; } } //时间O(NlogN) 空间O(1)
方法二:找数学规律 O(N*logN)提到到O(logN)
public class Solution { public int NumberOf1Between1AndN_Solution(int n) { int count =0; for(int i=1; i<=n; i*=10){//i每次*10,而不是+1 //复杂度O(logN) //i=1、10、100...分别对应个位、十位、百位的"1" //包含n int a = n/(i*10);//当前位之前的数 int b = n%i;//当前位之后的数 int c = (n/i)%10;//当前位 //整批 count += a*i; //末尾零散 if(c==1) count += (b+1); else if(c>=2) count += i; } return count; } }//时间O(logN) 空间O(1)
孩子们的游戏(圆圈中最后剩下的数)
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
方法一:朴素模拟法 O(m*n)
public class Solution { public int LastRemaining_Solution(int n, int m) { if(n<=0 || m<=0)return -1; ListNode head = new ListNode(0); ListNode p = head; for(int i=1; i<=n-1; ++i){ ListNode node = new ListNode(i);//串成链 p.next = node; p = p.next; } p.next = head;//p回到开头位置的前一个,形成闭环 //还有一个作用是,让p指向head前一个开始,可以使每轮循环一样套路 for(int i=1; i<=n-1; ++i){ for(int j=1; j<=m-1; ++j){ p=p.next; } p.next = p.next.next;//java会自动回收,所以不管那个被删除的节点 } return p.val;//剩下的最后一个 } }//这种思路是:模拟完整的游戏运行机制,不跳步 //时间O(m*n) 空间O(n)
方法二:数学归纳法 O(n) [分析是难点]
public class Solution { public int LastRemaining_Solution(int n, int m) {//时光倒流递推法:为什么要逆向?因为逆向是由少到多不会有空位;而正向会有空位,必须模拟、不能跳步 if(n<=0)return -1; int res = 0; //f(1,m)=0 for(int i=2; i<=n; i++){//i就是小朋友数量,i=2是游戏最后一轮,但是我解法的第一轮 //循环从i=2到i=n,小朋友越来越多,此解法是倒推(时光倒流) res = (res + m) % i; //相邻项关系:左边res是f(n,m) 右边res是f(n-1,m) } return res; } } //数学归纳法: //f(n,m)表示:【相对参考系:从0位置开始,最终到达f(n,m)位置】//例如:从0开始,最终到达f(5,3)=3的位置 //f(1,m)=0;//首项 //f(n,m)=[(m%n) + f(n-1,m)]%n;//公式化简为:f(n,m)=[m + f(n-1,m)]%n //相邻项关系【重点,推导如下】: //例如:f(5,3)=[f(4,3)+ 3%5 ] %5=f(4,3)+3 什么意思? //f(5,3)从0开始,0-1-2,删除2节点,然后来到3==》这时的情况就类似f(4,3)。但还有一点不一样,就是标准的f(4,3)从0开始,而这里从3开始 //f(4,3)根据定义,必须从0开始(所有的f(i,m)的定义需要一致),而不是从3。所以必须进行【对齐操作】: //于是f(5,3)的参靠系里:先走3步,然后以3为起点,走f(4,3)步 ==》f(5,3)=3+ f(4,3) [这里是简化,再考虑%n的细节优化下就ok了]
//有了递推公式,用递归法or迭代法,求解都几乎同理 //迭代法:f(1,m)=0 f(2,m)=[m + f(1,m)]%n ... 一直算到f(n,m)
//时间O(n) 空间O(1)
复杂链表的复制:较难
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
ps:原链表保持不变(改动后要还原)
/* public class RandomListNode { int label; RandomListNode next = null; RandomListNode random = null; RandomListNode(int label) { this.label = label; } } */ public class Solution {//画图:两个节点,4个状态的转换图;画完就好写程序了 //很重要一点就是判断.next .random是否为空==》很伤脑筋 public RandomListNode Clone(RandomListNode pHead) { if(pHead == null)return null; //第一步: RandomListNode pre = pHead; RandomListNode p = pHead.next;//p在pre之后 while(pre!=null){ RandomListNode node = new RandomListNode(pre.label); pre.next = node; node.next = p; pre = p; if(p==null)break; p=p.next; } RandomListNode head = pHead.next;//在第一步之后,head就容易获取了 //第二步: pre = pHead; while(pre!=null){ if(pre.random == null)pre.next.random = null;//不能漏了判定 else{ pre.next.random = pre.random.next;//经典 } pre=pre.next.next; } //第三步: p = head; pre = pHead; while(p.next != null){//这里的要求是"复制",所以必须也要还原 原链表 pre.next = p.next; p.next = p.next.next; pre=pre.next; p=p.next; } pre.next = null;//循环结束后的收尾(画图) return head; } }
链表中环的入口结点
/* public class Solution { //如果可以修改val的值,那么就很简单了,如下: public ListNode EntryNodeOfLoop(ListNode pHead) { if(pHead==null)return null; ListNode p=pHead; while(p!=null){ if (p.val==2199)return p; p.val=2199; p=p.next; } return null; } }//时间O(n) 空间O(1) //缺点是改变了原来链表的val */
import java.util.HashSet;//HashSet的查找时间是O(1),比用ArrayList的O(n)时间好 public class Solution { public ListNode EntryNodeOfLoop(ListNode pHead) { HashSet<ListNode> set = new HashSet<ListNode>(); //建立哈希表:存储、查找node //哈希表的特点:便于按值查找;但浪费空间,且易碰撞、无法排序 while(pHead != null){ if(set.contains(pHead))return pHead;//有重复,就说明是环入口 //可以证明,单链最多一个环 else{ set.add(pHead); pHead = pHead.next; } } return null;//无环单链 } }//时间O(n) 空间O(n) //如果用ArrayList的STL那么时间复杂度变为O(n^2)
方法二:双指针法(快指针2倍速) [最优解,证明是难点]
牛客解析辅助:
如果慢指针slow第一次走到了B点处,距离C点处还有距离Y,那么fast指针应该停留在D点处,且BD距离为Y(图中所示是假设快指针走了一圈就相遇,为了便于分析),也就是DB+BC=2Y,(因为fast一次走2步,慢指针一次走1步,并且相遇在C处)
在C点处,此时慢指针slow走的点为ABC,距离为X+Y,而快指针fast走的点为ABCDBC,距离为2X+2Y,
又因为:AB=X,BC=Y,快指针走了2次BC,所以CDB距离为X,而AB距离也为X。
public class Solution { public ListNode EntryNodeOfLoop(ListNode pHead) { ListNode fast = pHead; ListNode slow = pHead; while(true){ if(fast == null || fast.next == null)return null;//退出条件1:无环,fast走到null fast = fast.next.next;//fast二倍速 slow = slow.next;//fast的判空已经覆盖了slow; slow不需要单独判空 if(fast == slow)break;//退出条件2:有环相遇时 } ListNode front = fast;//front从fast==slow位置、一倍速 ListNode back = pHead;//back从头开始、一倍速 while(back != front){ back = back.next; front = front.next; } return front;//front==back } }//时间O(n)空间O(1) //构思巧妙,但复现不难。 //过程是这样的:双指针,fast每次走两步,slow每次走一步; //第一阶段,fast在前,slow在后,如果fast追上slow,说明有环。若fast走到null说明无环。 //第二阶段,back从头开始; front从fast==slow位置开始。现在front和back同速,恰巧可以在入口节点相遇。分析如上图【难点】
和为S的连续正数序列
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
双指针区间法(最优)
import java.util.ArrayList; public class Solution { public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) { ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); int left = 1; int right =1;//左闭右闭 int mySum = 1;//初始化 while(left <= sum/2){ if(mySum == sum){ ArrayList<Integer> list = new ArrayList<Integer>(); for(int i=left; i<=right; ++i){ list.add(i); } res.add(list); mySum -= left;//【相等也要移动】 ++left; } else if(mySum < sum){//two pointer滑动窗口机制-右边R ++right; mySum += right; } else if(mySum > sum){//two pointer滑动窗口机制-左边L mySum -= left; ++left; } } return res; } } //由于每轮L和R至少有一个加1,L<=sum/2,R<=1+sum/2 (比如sum=9,最终L=4,R=5) //所以while循环里面O(sum)轮 //但是每一轮的中如果mySum==sum (数量难以估计,且没有具体公式、且非单调变化),复杂度就很高 //如果mySum!=sum那么复杂度O(1) //综上,时间复杂度较难以量化(只能说在O(N)和O(N^2)之间) //除res之外,空间O(1)
不建议把程序写成这样:
import java.util.ArrayList; public class Solution { public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) { ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); int left = 1;int right =1;int mySum = 1;//一行定义多个变量,不符合代码风格规范(不建议这样做) while(left <= sum/2){ if(mySum == sum){ ArrayList<Integer> list = new ArrayList<Integer>(); for(int i=left; i<=right; ++i)list.add(i); res.add(list); } if(mySum < sum)mySum += ++right;//代码易读性下降,自己也容易写错(不建议这样做) else mySum -= left++;//这个else包含了<和==两种情况,其中==是要单独思考的情况;合在一块想节约代码行数,实则可读性大大下降(不建议这样做) } return res; } }
和为S的两个数字
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
ps:和上面一题类似。
//和上题比较类似,仍使用双指针法。不过不是区间和,而是两点之和 import java.util.ArrayList; public class Solution { public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) { ArrayList <Integer>list=new ArrayList <Integer>(); if (array.length<2)return list;//在不符合条件的时间要返回"空的list",而不是直接返回null int L=0;int R=array.length-1; while(L<R){ int SUM=array[L]+array[R]; if (SUM==sum){ list.add(array[L]); list.add(array[R]); return list;//遇到第一组即返回 } if(SUM<sum)L++; if(SUM>sum)R--; } return list;//此时为空,但不宜直接返回null } } //时间O(n) 空间O(1)
栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
import java.util.Stack; public class Solution { public boolean IsPopOrder(int [] pushA,int [] popA) { Stack<Integer> stack = new Stack<Integer>();//用栈模拟 int k = 0; for(int i=0; i<= pushA.length-1; ++i){//for和i针对push; while和k针对pop stack.push(pushA[i]); while(!stack.isEmpty() && stack.peek()==popA[k]){//出栈前先判空(好习惯) stack.pop(); ++k; } } if(k==popA.length)return true; return false; } } //时间O(n) 空间O(n)
第一个只出现一次的字符
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
//两步:第一步,扫一遍字符串,并记录出现次数;第二步,沿着str的顺序扫一遍存储的数组,返回第一个即可 public class Solution { public int FirstNotRepeatingChar(String str) { int [] hash=new int[128];//类型为int //大小写字母的范围在0~127内,允许部分冗余空间 for (int i=0;i<str.length();i++){//将String类型的str用i访问,类似数组;用.length()获得长度,容器STL的规范,而不是数组规范 hash[str.charAt(i)]++;//感觉很奇怪,hash是int类型的数组,竟然填进了char类型的下标。//隐含强制转换:Character=>Integer } for (int i=0;i<str.length();i++){ if (hash[str.charAt(i)]==1)return i;//注意str.charAt(i)的使用:将String类型的str用i访问、类似数组 } return -1; } } //时间O(n),O(1) //巧妙地利用了字符ASCII码的范围空间
字符流中第一个不重复的字符
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
public class Solution { int [] hash=new int[128];//经典128 ASCII码,128~255是一些符号扩展 String str = "";//不能写成:String str = null; ==>系统会得到str = "null"//Insert one char from stringstream public void Insert(char ch)//这里的意思应该是多次调用Insert函数,每次插入一个char { str=str+ch;//直接用String类型 + char类型的字符//用String记录下来一个个ch,在下面的函数遍历时候有用 hash[ch]++; }//时间O(1) //return the first appearence once char in current stringstream public char FirstAppearingOnce()//这个函数和上面的函数任意独立穿插调用使用 { for (int i=0;i<str.length();i++){ if (hash[str.charAt(i)]==1)return str.charAt(i); } return '#';//不能写return #,要写return '#';表示是ASCII字符而不是普通含义 }//时间O(N) }
方法一:LinkedHashMap <Character, Integer>
import java.util.LinkedHashMap; //必须要用LinkedHashMap,不能用HashMap/Map //因为LinkedHashMap的有序性(要求返回第一个char) public class Solution { LinkedHashMap <Character, Integer> map = new LinkedHashMap <Character, Integer>();//空间O(N) //Insert one char from stringstream public void Insert(char ch){//这里的意思应该是多次调用Insert函数,每次插入一个char if(map.containsKey(ch)){ map.put(ch,map.get(ch)+1); } else map.put(ch,1); }//时间O(N) 空间O(1) //return the first appearence once char in current stringstream public char FirstAppearingOnce(){//这个函数和上面的函数任意独立穿插调用使用 for(char ch:map.keySet()){//.keySet()获取map里面所有的key if(map.get(ch) == 1)return ch; } return '#'; }//时间O(N) 空间O(1) }
方法二(高效):int[128]存次数 + Queue< Character>
import java.util.LinkedList; public class Solution { int[] count = new int[128];//ASCII LinkedList<Character> queue = new LinkedList<Character>();//注意add/remove的使用 //空间O(N) public void Insert(char ch){ if(++count[ch]==1)queue.add(ch); }//时间O(1) 空间O(1) public char FirstAppearingOnce(){ while(queue.peek() != null){ if(count[queue.peek()]>1)queue.remove();//重复了就移出队列,它不会再回来了 else return queue.peek(); } return '#'; }//时间O(N) 如果FirstAppearingOnce()量大的话,可以接近O(1) //空间O(1) }
左旋转字符串
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
public class Solution { public String LeftRotateString(String str,int n) { if (str.length()<=0)return "";//对于str不能用null,要用"" int N=n%str.length(); return str.substring(N,str.length()) + str.substring(0,N);//str.substring(left,right)方法 } //复制Sting子串,左闭右开区间[left,right) }//时间O(n) 空间O(n) //因为复制substring需要系统的空间
字符串的排列
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
ps:提交前要排序
import java.util.ArrayList; import java.util.Collections; public class Solution { ArrayList<String> res = new ArrayList<String>(); public ArrayList<String> Permutation(String str) { if(str == null || str.equals(""))return res; exchange(str.toCharArray(), 0);//【str==>char[]】 //String格式转化为char数组,方便交换操作 Collections.sort(res);//最终输出前,要一次排序(字典序),不然每个人的答案不一样OG难以判断 return res; } public void exchange(char [] ch, int left){//传ch[]是因为这个被递归改成无数个版本副本,而res只有唯一版本 所以提到外面 if(left == ch.length-1){//left到了char[]数组的最后一个,才开始判重入列。否则长度不够的情况下都要继续向下分支 if(!res.contains(new String(ch))){//由于输入的str可能有重复字母,所以要去重;否则按照这个算法,如果原str无重复是不需要去重的(每个都能用得上) res.add(new String(ch));//要new String对象 //看下还有没有别的方式?? } } else{ for(int right=left; right<=ch.length-1; ++right){//从j=i开始,意思是第一个交换是,保留原来的ch,而并不会真正交换 swap(ch, left, right); exchange(ch, left+1);//深入 swap(ch, right, left);//还原,for循环下一轮还要用 原始版的 } } } public void swap(char [] ch, int i, int j){//swap不用返回,直接改ch[] char temp = ch[i]; ch[i] = ch[j]; ch[j] = temp; } } //【时间复杂度分析】 //下面的 N都是代表str的length, 比如 abc是3 //递归调用exchange()函数次数: N*(N-1)*(N-2)*...*1 = N!次 //判重:一共 N!次;每次的时间复杂度为O(N!) ==>因为要和res里面的所有str去比较 //综上,本题时间复杂度为 O(N!*N!) //【空间复杂度分析】 //如果包括系统栈空间的话,每个递归exchange()函数都需要一个ch[],其大小为N //那么总空间为:O(N!)*O(N) = O((N+1)!)
正则表达式匹配
请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配
方法一:递归
1.节省时间空间版:
public class Solution { char[] Str;//全局变量提出来 char[] Pattern; public boolean match (String str, String pattern) { if(str == null || pattern == null)return true; Str = str.toCharArray(); Pattern = pattern.toCharArray(); return match(0,0); } public boolean match (int s, int p){ //先处理能直接判断true/false的情况: if(s == Str.length && p == Pattern.length)return true; if(s != Str.length && p == Pattern.length)return false; if(p+1 <= Pattern.length-1 && Pattern[p+1] == '*'){ if(s != Str.length && (Pattern[p] == Str[s] || Pattern[p] == '.') ){//当前字符匹配 return (match(s+1,p))||(match(s,p+2));//匹配时,可能有(跳过/不跳过)两种可能分支 } else return match(s,p+2);//不匹配,跳过_* } else{ if(s != Str.length && (Pattern[p] == Str[s] || Pattern[p] == '.') ){//当前字符匹配 return match(s+1,p+1); } else return false; } } }
2.代码简短版(使用substring):
public class Solution {//便于记忆 public boolean match (String str, String pattern) { if (str.equals(pattern))return true; boolean isFirstMatch = false; if (!str.isEmpty() && !pattern.isEmpty() && (str.charAt(0) == pattern.charAt(0) || pattern.charAt(0) == '.')) { isFirstMatch = true; } if (pattern.length() >= 2 && pattern.charAt(1) == '*') { return (isFirstMatch && match(str.substring(1), pattern)) || match(str, pattern.substring(2)); } return isFirstMatch && match(str.substring(1), pattern.substring(1)); } }
//递归:子结构有着相同算法的;使用递归,使代码简洁;
//若子结构计算无重复,算法不会产生递归的副作用。
//本题中,在递归分支时候,会产生一些重复计算==>所以时间方面会多消耗,可以通过增加二维数组的方式(dp=递归+存储)来空间换时间
方法二:动态规划(似乎没有明显优势)
public class Solution { public boolean match (String str, String pattern) { int n = str.length(); int m = pattern.length(); boolean[][] dp = new boolean[n + 1][m + 1]; dp[0][0] = true; for (int i = 1; i < n; i++) { dp[i][0] = false; } for (int i = 0; i <= n; i++) { for (int j = 1; j <= m; j++) { if (pattern.charAt(j - 1) != '*') { if (i > 0) { if ( str.charAt(i - 1) == pattern.charAt(j - 1) || pattern.charAt(j - 1) == '.') { dp[i][j] = dp[i - 1][j - 1]; } } } else { if (j >= 2) { dp[i][j] = dp[i][j - 2]; } if (i >= 1 && j >= 2) { if ( str.charAt(i - 1) == pattern.charAt(j - 1) || pattern.charAt(j - 1) == '.') { dp[i][j] = dp[i][j] || dp[i - 1][j]; } } } } } return dp[n][m]; } }
把字符串转换成整数
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。
例如:+2147483647(String) ==》2147483647(Int)
ps:没有科学计数法
public class Solution { public int StrToInt(String str) { if(str==null || str.length()==0)return 0; char[] ch = str.toCharArray(); int flag = 1;//无符号默认是1 if(ch[0] == '-'){ flag = -1; ch[0] = '0'; } if(ch[0] == '+'){ ch[0] = '0';//+-号置为零 } int res = 0; for(int i=0; i<=str.length()-1; ++i){ if(ch[i]-'0' <0 || ch[i]-'0' >9){ return 0;//出现非法字符 } else{ res*=10;//乘10放在+之前就不用判断了 res += (ch[i]-'0'); } } return res*=flag; } }//时间O(n),空间O(n)
表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。
ps:都是固定格式套路
(1)正则表达式 解法
比较死板的规则可以用正则化表达式,如果过于复杂就要写if else for的语句了
public class Solution { public boolean isNumeric (String str) { return str.matches("[+-]?((\d+(\.\d+)?)|(\d*\.\d+))([Ee][+-]?\d+)?"); //别忘了双引号"",因为s匹配的是字符串 } }
正则表达式专题:
// 正则表达式: // []表示并列选其一;()表示同时全都要; // ?修饰[]、(),表示 0次/1次 // +表示1次-多次 *表示0次-多次 // |或 &与 // d表示[0-9],用法:\d 需要字母直接写d表示'd'; // 标点符号如:+-*/.等都要反斜杠\来转义 // ^表示开头 $表示结尾
(2)普通流程法
//普通方法又臭又长,看不上,先不写了...
不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
//位操作,使用二进制来计算,模拟加法 public class Solution { public int Add(int num1,int num2) {//num1和num2是形参,在函数里面可以直接用 int and = 2199;//与,进位值 //初值设为非零 int xor = 0;//异或,非进位值 while(and!=0){//当and==0【已经无进位了】的时候退出循环,此时xor的值就是最终的和【核心思想】 and=(num1&num2)<<1;//先与,再左移,表示进位。 xor=(num1^num2); num1=and; num2=xor; } return xor; }//总结一下4种位操作和<<移位操作【JAVA 6种】:与(&) 或(|) 非/取反(~) 异或(^) 左移(<<) 右移(>>) }// and or not xor //位操作只能用于整形,不能用于float和double //位操作的优先级比较靠后,比加减还要后,所以要多打括号。
翻转单词顺序列
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
方法1:
//从后向前扫描,将单词转移到另一个O(n)空间String public class Solution { public String ReverseSentence(String str) { if(str==null||str.length()==0)return "";//空的时候不要返回null,要用"" char[]s=str.toCharArray();
if(s[0]==' ')return str;//这行 是看别人题解里面的补丁,真是觉得题目故意搞这些奇怪的测试用例。。醉醉的。。
String result=new String();//""//new String(),new的时候,不要忘记这个括号 for (int i=str.length()-1;i>=0;i--){ if(i==0||s[i-1]==' '){//i==0写在前面,防止i-1溢出 int k=i;// while(k!=str.length()&&s[k]!=' '){ result+=s[k];//String直接加char k++; } if(i!=0)result+=' ';//原句最前面一个词后面不加空格 } } return result; } }
//时间O(n) 空间O(n)
方法2:两轮翻转法
//首先完全翻转,然后逐个单词翻转 //空间复杂度:C++直接操作string,复杂度O(1) Java要toCharArray,复杂度O(n) //复习时候考虑再写下,先过了
把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
方法1:base on 冒泡排序
public class Solution { public String PrintMinNumber(int [] numbers) { for(int i=0;i<numbers.length-1;i++){ for (int j=0;j<numbers.length-1-i;j++){//排成最小数字==>字典序最小 //【贪心算法思想:局部最优到全局最优】 //long x1=Integer.valueOf(numbers[j]+""+numbers[j+1]);//中间的位置有个""空字符串,不然int直接相加了 //long x2=Integer.valueOf(String.valueOf(numbers[j+1])+""+String.valueOf(numbers[j]));//两种写法,本行是规范版 //String->int,使用Integer.valueOf(); int->String使用String.valueOf() //两次转换 String x1=numbers[j]+""+numbers[j+1]; String x2=numbers[j+1]+""+numbers[j]; if(x1.compareTo(x2)>0){//举例子知道 大于小于号 //对于String之间的比较,使用str1.compareTo(str2) 若str1>str2则返回正。 1<2负 1==2返回0 //if()里面必须boolean类型 int temp=numbers[j]; numbers[j]=numbers[j+1]; numbers[j+1]=temp; } } } String result=""; for (int i=0;i<numbers.length;i++){ //result+=String.valueOf(numbers[i]);//String.valueOf() 将int转化为String result+=numbers[i];//可以直接将int加给String } return result; } }
//时间O(n^2) 空间O(n)
方法2:base on 快排
import java.util.ArrayList; public class Solution { public String PrintMinNumber(int [] numbers) { String res =""; Partition(0, numbers.length-1, numbers); for(int i=0; i<=numbers.length-1; ++i){ res += numbers[i]; } return res; } public void Partition(int left, int right, int [] numbers){ if(left < right){ int l = left; int r = right; int temp = numbers[left]; while(left < right){ while(left < right && ((""+ temp + numbers[right]).compareTo(""+ numbers[right] + temp)<=0)) --right;//快排永远的等号 numbers[left] = numbers[right]; while(left < right && (("" + numbers[left] + temp).compareTo(""+ temp + numbers[left])<=0)) ++left; numbers[right] = numbers[left]; } numbers[left] = temp; int divide = left; Partition(l, divide-1, numbers); Partition(divide+1, r, numbers); } } }//时间O(NlogN) 空间O(N)
数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
方法:参照归并排序:逆序->升序 的过程中不断求局部的逆序数
public class Solution {//首先想到求逆序对的过程就是:将原始数组调整顺变成升序的排列,也就是排序。 //从三种O(nlogn)的方法里面找,又因为快排、堆排序都是相邻不稳定,故排除; int count = 0; public int InversePairs(int [] array) { divide(array, 0, array.length-1); return count;//逆序数 } public void divide(int [] array, int left, int right){ if(left < right){//递归结束条件:最少两个才分 int mid = (left + right)/2; divide(array, left, mid); divide(array, mid+1, right); merge(array, left, right, mid); } } public void merge(int [] array, int left, int right, int mid){//array[]也是不断变动的,所以递归必传此值 int [] temp = new int [right-left+1];//归并后的大数组 int k = 0;//temp数组下标 int l = left;//l对应左数组,r对应右数组 //本函数全局使用 int r = mid+1; while(l<=mid && r<=right){ if(array[l] < array[r]){ temp[k++] = array[l++]; } else{ temp[k++] = array[r++]; count += ((mid+1)-l);//右数组穿过左数组的元素的个数(==左数组剩下元素个数)【全文重点】 count %= 1000000007;//取余数(如果size范围再稍小一些,就可以将这个提到while循环外面,从而提高效率) } } while(l<=mid)temp[k++] = array[l++]; while(r<=right)temp[k++] = array[r++]; k=0; for(int i=left; i<=right; ++i){ array[i] = temp[k++];//赋值回到array } } } //时间复杂度:O(NlogN) //空间复杂度:O(N) //temp数组每个递归函数用完释放
扑克牌顺子
现在有2副扑克牌,从扑克牌中随机五张扑克牌,我们需要来判断一下是不是顺子。
有如下规则:
1. A为1,J为11,Q为12,K为13,A不能视为14
2. 大、小王为 0,0可以看作任意牌
3. 如果给出的五张牌能组成顺子(即这五张牌是连续的)就输出true,否则就输出false。
例如:给出数据[6,0,2,0,4]
中间的两个0一个看作3,一个看作5 。即:[6,3,2,5,4]
这样这五张牌在[2,6]区间连续,输出true
数据保证每组5个数字,每组最多含有4个零,数组的数取值为 [0, 13]
方法1:朴素模拟法——king补充空位
public class Solution { public boolean IsContinuous(int [] numbers) { int[] count = new int[14];//0~13,普通牌卡槽 int kingCount = 0; for(int i=0; i<=4; ++i){ if(numbers[i] == 0){ ++kingCount; } else{ if(count[numbers[i]] == 1)return false;//有重复,直接false,不可能同花顺了 count[numbers[i]] = 1; } } int num =0;//连续个数 for(int i=1; i<=13; ++i){//遍历count[]数组 if(count[i] == 1) ++num; else if(num!=0 || i>=9 && num<i-8){//补位:普通补位+末尾补位(末尾没有count,也要提前用king的情况) if(kingCount == 0)return false; else{ --kingCount; ++num; } } if(num == 5)return true;//及时 } return false; } }
方法2:利用特性——卡不重复时,max-min<=4即可
public class Solution { public boolean IsContinuous(int [] numbers) { int count[] = new int[14];//正好对应0-13的卡牌(卡牌号->次数) int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE;//把min设置为最大,max设置最小 for(int i=0; i<5; ++i){ if(numbers[i]!=0){//0不参加 ++count[numbers[i]]; if(count[numbers[i]]>=2)return false;//保证普通牌无重复 if(numbers[i] > max) max=numbers[i]; if(numbers[i] < min) min=numbers[i]; } } if(max - min <= 4)return true; return false; } }//时间O(1) 空间O(1)
查重:上面是用数组,下面是用Set
import java.util.HashSet; public class Solution { public boolean IsContinuous(int [] numbers) { HashSet<Integer> set = new HashSet<Integer>();//HashSet int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; for(int i=0; i<5; ++i){ if(numbers[i]!=0){ if(!set.add(numbers[i]))return false;//HashSet if(numbers[i] > max) max=numbers[i]; if(numbers[i] < min) min=numbers[i]; } } if(max - min <= 4)return true; return false; } }
树的子结构
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
ps:大的思路不算难,但这里面细节比较多
public class Solution {//分两步: //[1]遍历root1树,尝试每一个节点 public boolean HasSubtree(TreeNode root1,TreeNode root2) { if(root1==null || root2==null)return false;//由题,root1或root2初试为null都会导致false if(judge(root1,root2)==true)return true;//必须要有if判断 ==>只有true才返回、并结束题目任务;false时不能返回,并进行下方的详细判别 return HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);//这里的关系是"或" } //表示整个树的所有分支只要有一个末端分支满足即可。 //[2]针对某一节点node,判断是否与root2匹配 public boolean judge(TreeNode node, TreeNode root2){ if(root2==null)return true;//在前,因为有:node和root2都为null的情况,root2为空node不为空的情况(本题允许在匹配时,子树比原树下方短) if(node==null)return false;//在后,相当与node==null&&root2!=null if(node.val != root2.val)return false;//不相等直接结束,否则继续向下详细检查 return judge(node.left,root2.left) && judge(node.right,root2.right);//"与"的关系,表示子树所有分支全部都要满足。 } } //judge()函数复杂度为O(root2) //root2是B树(子树) //HasSubtree()由于每个root1树的节点都要试一下,调用次数O(root1) //==>时间复杂度 O(root1*root2)
平衡二叉树
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
ps:题目有漏洞,没有判断树的中序遍历,只判断了树的深度一个方面。
方法1:朴素求深度
public class Solution { private boolean flag=true;//全局变量最好放在主函数外面,这样不用在函数接口里面传来传去,直接所有函数都能用 public boolean IsBalanced_Solution(TreeNode root) {//题目中只需要判断深度符合平衡就OK了 deepth(root); return flag; } private int deepth(TreeNode node){//depth//主函数返回类型(boolean)不适合递归求深度,于是新建函数(int)来递归 if(node==null)return 0; int x1=deepth(node.left); int x2=deepth(node.right); if((x1-x2)>1||(x2-x1)>1)flag=false;//表示高度差失衡 //也可以用 Math.abs(x1-x2)>1 return x1>x2?x1+1:x2+1; //也可以用:1+Math.max(x1,x2) } }
方法2:剪枝大法
public class Solution { public boolean IsBalanced_Solution(TreeNode root) { return depth(root)!=-1;//是个判定语句,将int转化为true/false } private int depth(TreeNode node){//当高度失衡的时候直接返回-1,log层结束递归,而不是继续返回depth if(node==null)return 0; int x1=depth(node.left); if(x1==-1)return -1;//如果后序遍历发现问题,就直接从发现问题的点log次向上,直接返回了。左子树后面直接有return-1的地方,不用等右子树。 int x2=depth(node.right); if(x2==-1)return -1; int diff=Math.abs(x1-x2);//Math不用import int deep=Math.max(x1,x2)+1; return diff>1?-1:deep; } }
对称的二叉树
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。
public class Solution { boolean isSymmetrical(TreeNode pRoot) { if(pRoot == null)return true; return sym(pRoot.left, pRoot.right); } boolean sym(TreeNode left, TreeNode right){//新拉出一个函数,是因为要设置两个node指针 if(left==null && right==null)return true; if(left==null || right==null)return false;//在上一句不全为null的前提下,1空1实 =>false if(left.val != right.val)return false; //上面3行是本层的校验,如果没有分出true/false,那么继续向下递归: return sym(left.left,right.right) && sym(left.right,right.left);//【重点】注意和普通DFS的区别 } }
把二叉树打印成多行
从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。
方法1:朴素层次遍历(用queue辅助)
import java.util.ArrayList; public class Solution { ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) { ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); if(pRoot == null)return res; ArrayList<TreeNode> queue = new ArrayList<TreeNode>(); queue.add(pRoot); while(!queue.isEmpty()){ ArrayList<Integer> list = new ArrayList<Integer>(); int size = queue.size(); for(int i=0; i<=size-1; ++i){ TreeNode node = queue.get(0); queue.remove(0); list.add(node.val); if(node.left != null)queue.add(node.left); if(node.right != null)queue.add(node.right); } res.add(list); } return res; } }//层次遍历:时间空间均为O(N)
方法2:dfs递归遍历
由于有ArrayList记录,使用递归的方法遍历 //无论先序、中序、后序,对于某一层而言,都是严格一个个从左到右
import java.util.ArrayList; public class Solution { ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) { if(pRoot == null)return res; dfs(pRoot,0); return res; } void dfs(TreeNode node, int layer){ if(node == null)return; if(layer >= res.size()){//注意这里有"="的情况 res.add(new ArrayList<Integer>()); } res.get(layer).add(node.val);//【这里的dfs用先序/中序/后序,都可以的】 dfs(node.left, layer+1); dfs(node.right, layer+1); } }//dfs:时间、空间都是O(N) //res是全局变量 //layer和node都不是全局变量
二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
public class Solution { //1.拆分函数问题(如果递归时直接传入子数组,则开头出现同样的判断,但由于初次结果和后续不一样==>flag法没用,必须拆分) //2.递归拆分问题:由于原数组不需要修改,所以不用新占用空间来复制备份子数组,而是只要传入左右下标即可 public boolean VerifySquenceOfBST(int [] sequence){ if(sequence.length == 0)return false; return judge(sequence, 0, sequence.length-1); } boolean judge(int [] a, int left, int right){ if(left>=right)return true;//分支结束两种情况:l==r单个叶;l>r为空 //这两个情况都是true int mid = 0; while(a[right]>a[mid])++mid; for(int i=mid+1;i<=right;++i)if(a[i]<a[right])return false;//发现问题直接false,否则继续往下挖 return judge(a,left,mid-1) && judge(a,mid,right-1); } } //复杂度分析:每个递归主体会减少一个节点,所以需要N次调用judge()函数; //每次judge()函数:1) 时间上会遍历一轮数组 2) 空间上新定义几个固定变量(sequence一直没有去复制它,而是通过形参引用) //所以总时间复杂度为 O(n^2)、空间复杂度为 O(n)
二叉树的下一个节点
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
public class Solution { public TreeLinkNode GetNext(TreeLinkNode pNode) { if(pNode == null)return null; if(pNode.right != null){//pNode有右子树,(中序后继)则一定在右子树里面的(最左边的一个) pNode = pNode.right;//从右子树根开始 while(pNode.left != null)pNode = pNode.left; return pNode; } else{//这个else只为了层次清晰,无实际意义。表示pNode没有右子树的情况。 while(pNode.next != null && pNode.next.left != pNode)pNode = pNode.next; //向上溯源,找第一个node是“node父节点的左孩子”的(中序能戳到屁股) return pNode.next;//包含:退到根节点 仍然找不到后继,返回null的情况 } } }//时间O(logN) /*特殊的节点定义:比平常的节点多一个指向父节点的指针 如果没有这个特殊的指针,就只能中序递归了, 时间复杂度O(N) public class TreeLinkNode { int val; TreeLinkNode left = null; TreeLinkNode right = null; TreeLinkNode next = null; //父节点 TreeLinkNode(int val) { this.val = val; } }*/
从上往下打印二叉树
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
ps:层次遍历
import java.util.ArrayList; public class Solution { public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) { ArrayList<Integer> result=new ArrayList<Integer>();//Integer//存储结果,只进不出 ArrayList<TreeNode> queue=new ArrayList<TreeNode>();//TreeNode//用于层次遍历的队列:出1进0~2 if(root==null)return result;//不要直接返回null,不符合格式,要返回空的ArrayList<Integer>result; queue.add(root); int q=0;//计数queue //int r=0;//计数result while(q<=queue.size()-1){//ArrayList[STL]没有.length(),只有.size() ?? TreeNode node=queue.get(q);//当前node节点 result.add(node.val);// if(node.left!=null)queue.add(node.left); if(node.right!=null)queue.add(node.right); q++;//就不弹出了,直接往下走 } return result; } }
上面是辅助队列queue不删除用过节点,
下面用完就删(理论上空间效率提高、时间效率下降):
ps:实际上OG里面,两种方法时间、空间都差不多。
import java.util.ArrayList; public class Solution { public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) { ArrayList<Integer>result=new ArrayList<>(); ArrayList<TreeNode>queue=new ArrayList<>(); if(root==null)return result; queue.add(root); while(queue.size()!=0){ TreeNode node=queue.get(0);//永远从queue的第一个node开始 queue.remove(0);//用完删除 //使用remove来删除 //没有delete这个方法。。 result.add(node.val); if(node.left!=null)queue.add(node.left); if(node.right!=null)queue.add(node.right); } return result; } }
使用LinkedList的丰富API来解决队列问题:
import java.util.ArrayList; import java.util.LinkedList;//增查删:add peek remove //头尾:First Last public class Solution { public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) { ArrayList<Integer> res = new ArrayList<Integer>(); LinkedList<TreeNode> queue = new LinkedList<TreeNode>();//TreeNode //用于层次遍历的队列:出1进0~2 //【LinkedList】用于实现队列,有头尾操作的API if(root==null)return res;//不要直接返回null,不符合格式,要返回空的ArrayList<Integer>result; queue.addFirst(root); while(!queue.isEmpty()){ TreeNode node = queue.peekFirst();//多次复用,提高代码可读性 res.add(node.val); if(node.left != null)queue.addLast(node.left); if(node.right != null)queue.addLast(node.right); queue.removeFirst(); } return res; } } //时间空间复杂度均为:树的规模 O(n)
二叉树中和为某一值的路径
输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)
方法1:使用void函数(方法)来递归修改全局变量
import java.util.ArrayList; public class Solution { private ArrayList<ArrayList<Integer>> result=new ArrayList<ArrayList<Integer>>();//全局一个版本,被所有递归分支修改,不用传来传去 private ArrayList<Integer> list=new ArrayList<Integer>();//【借鉴精华】全局设置一个list,被所有递归分支改来改去 //【借鉴精华】全局设置一个list,只要在离开一个节点的时候(左右子树递归完成并彻底离开)删除这个节点的val就可以始终保持为单分支的路径 public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) { if(root==null)return result; Find(root,target); return result;//如果没有改动,即没有找到路径,则result保持为空ArrayList。 } private void Find(TreeNode node, int target){//void类型,只需要修改全局的result和list
//target不是全局变量,是局部的上下层相对变量,递归函数中改来改去产生很多栈中的备份 if(node!=null){ target-=node.val; list.add(node.val);//路径list入值 if(target==0 && node.left==null && node.right==null){//找到了【必须满足左右子树为null】 int i=result.size()-1;//要放在外面,因为不止for循环里面要用 while(i>=0 && result.get(i).size()<list.size())i--;//从原先的最后一个开始,一直比到第0个 //如果原先为空,则不会进入while循环 //找到位置 //list放入result //【类似插入排序】 result.add(i+1,new ArrayList<Integer>(list));//i+1的位置//add(index,内容) 指定位置的add }//【add添加的是引用,如果不new一个的话,后面的操作会更改这个list】这里面要用new ArrayList<Integer>(list)【这样等于有备份】,不能直接用list //不能直接用list,不然最后list不断改变时,result里面的引用全部指向最终的list,这也就是我一开始百思不得其解的原因 if(target>0){//继续向下//如果不判断的话,也不影响结果(如果val非负)。但是这可以剪枝target<0的情况,不然全部都要递归到叶节点,浪费很多时间。 Find(node.left,target); Find(node.right,target); } //target<0时,不需要任何操作 //list是一维数组,这里是删除最近访问的一个node.val。一条主线深入向下探索树,向上时必须回退: list.remove(list.size()-1);//【离开时删除(回退),全文重点】 } } }
方法2:使用return result来递归(不用去新建一个函数)
import java.util.ArrayList;//和上面那个方法差不多,不用分出来写个函数 public class Solution { private ArrayList<ArrayList<Integer>> result =new ArrayList<ArrayList<Integer>>(); private ArrayList<Integer> list=new ArrayList<Integer>(); public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) { if(root==null)return result; target-=root.val; list.add(root.val); if(target==0 && root.left==null && root.right==null){ int i=result.size()-1; while(i>=0 && result.get(i).size()<list.size())i--; result.add(new ArrayList<Integer>(list)); } if(target>0){ FindPath(root.left,target); FindPath(root.right,target); } list.remove(list.size()-1); return result; } }
二叉搜索树的第k个结点
给定一棵二叉搜索树,请找出其中的第k小的结点。例如,(5,3,7,2,4,6,8)中,按结点数值大小顺序第三小结点的值为4。
方法一:双函数(新建的void函数 用于修改全局变量res;主函数负责return)
public class Solution { private int count = 0;//全局(类)变量可以不初始化,int默认为0 //等价于private int count; private TreeNode res = null;//等价于private TreeNode res; //全局(类)变量不能这样初始化:private TreeNode result=new TreeNode();这种方法用于函数内局部变量初始化 TreeNode KthNode(TreeNode pRoot, int k){ count = k; Find(pRoot); return res; } void Find(TreeNode pRoot){ if(pRoot!=null && res==null){//res==null用于剪枝加速;本题不剪枝也答案正确 //也可以用count>=0来剪枝 Find(pRoot.left); if(--count == 0)res=pRoot; Find(pRoot.right); } } }//时间O(N) 空间O(logN) //指导思想:【树递归,能拆就拆】
//拆开的三大好处:接口清晰、时空高效、简化逻辑(不易写错)==>对人对机器都好
方法二:单个函数
public class Solution { int count = 0; TreeNode KthNode(TreeNode pRoot, int k) {//单函数:代码短,但逻辑复杂 if(pRoot != null){//dfs的剪枝尽量不要分左右子树来写,这样集中写比较好 TreeNode left = KthNode(pRoot.left,k); if(left != null)return left;//帮助返回res,并拦截内层的null if(++count == k)return pRoot;//【一旦找到,就会剪枝、logn时间迅速向上】//找不到就一直没有return TreeNode right = KthNode(pRoot.right,k); if(right != null)return right;//帮助返回res,并拦截内层的null } return null;//最外层的null才可能返回,其他都会被拦截 } }//时间O(N) 空间O(logN)
按之字形顺序打印二叉树
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
ps:(1)本题类似于:层次遍历二叉树。 (2)"之" 就是 "S"型 蛇形。
方法1:使用一个队列(用于层次遍历:所有层),加一个栈(用于高效翻转:偶数层)
import java.util.ArrayList; import java.util.Stack; public class Solution { public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) { ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); if(pRoot == null)return res; Stack<Integer> stack = new Stack<Integer>();//stack仅用于偶数层翻转val ArrayList<TreeNode> queue = new ArrayList<TreeNode>();//queue是奇偶共用 queue.add(pRoot); int layer = 1;//层数用layer (区别于深度depth = layer-1) while(!queue.isEmpty()){ ArrayList<Integer> list = new ArrayList<Integer>();//新建行 int size = queue.size();//出本层前记录size,不然难以做到层数的切分 //提前写出来,因为size会变 for(int i=0; i<=size-1; ++i){ TreeNode node = queue.get(0);//一定要新建node副本,不然是引用会变 queue.remove(0); if(layer % 2 == 1){ list.add(node.val); } else{//偶数行,需要栈翻转 stack.push(node.val); } if(node.left != null)queue.add(node.left); if(node.right != null)queue.add(node.right); } while(!stack.isEmpty()){ list.add(stack.pop());//偶数层一次性添加,奇数层一个个添加 } res.add(list); ++layer;//本层结束,层数加一 } return res; } }//时间、空间复杂度,都是树的规模 O(N)
方法2:使用一个队列(用于层次遍历),加一个ArrayList(偶行、奇行分别头插、尾插)
import java.util.ArrayList; public class Solution {//方法2和方法1相比,代码区别仅仅是把stack相关删去,然后换成ArrayList头插法 public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) { ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>(); if(pRoot == null)return res; ArrayList<TreeNode> queue = new ArrayList<TreeNode>(); queue.add(pRoot); int layer = 1; while(!queue.isEmpty()){ ArrayList<Integer> list = new ArrayList<Integer>(); int size = queue.size(); for(int i=0; i<=size-1; ++i){ TreeNode node = queue.get(0); queue.remove(0); if(layer % 2 == 1){ list.add(node.val); } else{ list.add(0,node.val);//头插法,逆序 //【代码简洁,但效率比Stack低】 } if(node.left != null)queue.add(node.left); if(node.right != null)queue.add(node.right); } res.add(list); ++layer; } return res; } }//时间、空间都是 O(N)
二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
方法一:非递归(用栈)
import java.util.Stack;public class Solution {//二叉树本来就有left、right两个指针位置,初始状态有的为空有的为null public TreeNode Convert(TreeNode root) {//pRootOfTree->root 可以把原来的名字改短 if(root==null)return null; TreeNode p=root; TreeNode pre=null;//双指针,p前面一个指针 TreeNode head=null;//返回中序第一个节点 Stack<TreeNode> stack=new Stack<TreeNode>();//中序【非递归】,使用【辅助栈】 while(p!=null||!stack.isEmpty()){//两个不都为空 //作为STL的stack进行判断时,一般不会直接判断:stack!=null,而是用isEmpty() //-----------------------------------------------------向左0~n次 while(p!=null){//【记住:这里的判断语句是 p!=null】(经验),而不是p.left!=null,不然后面向右转后 会出问题 stack.push(p);//push进栈,保存节点 //注意与下面一句的先后顺序 p=p.left; } //-----------------------------------------------------中序的处理 p=stack.pop();//【pop: remove &return】 //peek:return if(pre==null){//首次 head=p;//需要设置头部head } else{//中间和末尾情况下:操作一样,所以不用拆分 //画图分析:【先考虑中间部分,再考虑头尾】 pre.right=p; p.left=pre; } pre=p;//所有情况都需要//此时p能保证不为null //------------------------------------------------------向右1次 p=p.right;//可能为null,此时到下一轮直接miss向左的过程 } return head; } }
方法二:中序递归
1) 常规版,需要保存头结点
public class Solution { TreeNode pre = null;//像这种相邻关系的,设置pre指针 TreeNode head = null; public TreeNode Convert(TreeNode pRootOfTree) { if(pRootOfTree != null){ Convert(pRootOfTree.left); //由于是中序遍历,所以自动走出有序的顺序 if(pre == null)head = pRootOfTree;//第一次保存head else{//后面n-1次情况 pre.right = pRootOfTree; pRootOfTree.left = pre; } pre = pRootOfTree;//pre这个全局变量,随着中序的pRootOfTree前进一个节点 Convert(pRootOfTree.right); } return head;//只有最外层的return是用上的 } } //时间O(N) 空间O(1)
2) 升级版:先右后左
public class Solution { TreeNode pre =null; public TreeNode Convert(TreeNode pRootOfTree) { if(pRootOfTree !=null){ Convert(pRootOfTree.right);//先右后左 if(pre != null){ pRootOfTree.right = pre; pre.left = pRootOfTree; } pre = pRootOfTree; Convert(pRootOfTree.left); } return pre; } }
方法三:先全按顺序存数组,然后再设置指针
import java.util.ArrayList; public class Solution { ArrayList<TreeNode> sortList = new ArrayList<TreeNode>(); public TreeNode Convert(TreeNode pRootOfTree) { if(pRootOfTree == null)return null; sort(pRootOfTree); set(); return sortList.get(0); } public void sort(TreeNode pRootOfTree){ if(pRootOfTree.left != null)sort(pRootOfTree.left); sortList.add(pRootOfTree);//坐实了:add只会存引用,而不是新建 if(pRootOfTree.right != null)sort(pRootOfTree.right); } public void set(){ for(int i=1; i<=sortList.size()-1; ++i){ sortList.get(i-1).right = sortList.get(i); sortList.get(i).left = sortList.get(i-1); } } } //时间O(N) 空间O(N)
序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树:
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
public class Solution {//1、序列化 String str = ""; String Serialize(TreeNode root) { serHelper(root); return str; } void serHelper(TreeNode root){ if(root != null){ str += String.valueOf(root.val);//Integer转为String str += ","; Serialize(root.left); Serialize(root.right); } else{ str += "#,"; } } //2、反序列化 String[] s = null; int i = -1;//标记String[] s 的下标,关键! TreeNode Deserialize(String str) { s = str.split(",");//将String用','分割split开,存在String数组里 return desHelper(); } TreeNode desHelper(){ ++i; if(!s[i].equals("#")){ TreeNode node = new TreeNode(Integer.valueOf(s[i]));//先新建node再添加left、right指针==>所以用先序遍历 node.left = desHelper(); node.right = desHelper(); return node; } else return null; } } //复杂度分析: //dfs==> 时间O(N) (除res外的)空间O(logN)
//还可以通过bfs层次遍历的办法来 序列化/反序列化: //bfs==> 时间O(N) (除res外的)空间O(N) //辅助队列Queue的O(N/2)==O(N) //dfs先序的方法比bfs层次遍历要简便、节省空间
绕过OG的外挂方法:
//此方法不能实现功能,只能绕过OG系统 public class Solution { TreeNode res = null; String Serialize(TreeNode root) { res = root; return "(*^_^*)"; } TreeNode Deserialize(String str) { return res; } }
构建乘积数组
给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * ... * A[n-1],B[n-1] = A[0] * A[1] * ... * A[n-2];)
import java.util.ArrayList; public class Solution { public int[] multiply(int[] A) { int[] B = new int[A.length]; int left = 1;//left和right分别代表两个三角半区,都必须是越来越大(题目不给除法) int right =1; for(int i=0; i<=A.length-1; ++i){ B[i] = left; left *= A[i]; } for(int i=A.length-1; i>=0; --i){ B[i] *= right; right *= A[i]; } return B; } }//时间O(N) 空间O(N) //【空间复杂度:运行中临时占用的存储空间】
数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
总体思路:
设立一个大根堆、一个小根堆,还有一个奇偶标记。
大根堆保存较小的半边,小根堆大的半边;入堆来回倒(以保证中位)、出堆类型转换
import java.util.PriorityQueue; //Java的PriorityQueue基于堆 //add()添加 poll()弹出顶端元素=>时间都是O(logN) //peek()查看顶端元素=>时间O(1) //【poll】比较特别,相当于栈的pop public class Solution { PriorityQueue<Integer> minRootHeap = new PriorityQueue<Integer>();//小根堆 PriorityQueue<Integer> maxRootHeap = new PriorityQueue<Integer>((x,y) -> y-x);//大根堆:用lambda表达式 调整顺序 boolean isOdd = true;//可以设置boolean,也可以设置一个Int类型的++i //3个全局(类)变量,空间复杂度O(N) public void Insert(Integer num) { if(isOdd){ minRootHeap.add(num);//插入小根堆 //由于是中位数,所以是对称的,反过来也可 maxRootHeap.add(minRootHeap.poll());//小根堆最小值,给到大根堆 } else{ maxRootHeap.add(num); minRootHeap.add(maxRootHeap.poll()); } isOdd = !isOdd; }//时间O(logN) 空间O(1) public Double GetMedian() {//输出的类型是Double if(!isOdd){//上面函数最后转换了isOdd,所以前面有一个 !【重要】 return maxRootHeap.peek() / 1.0;//【强制转换】成Double } else{ return (maxRootHeap.peek() + minRootHeap.peek()) / 2.0; } }//时间O(1) 空间O(1) }
【无序数组中,找中位数】
在无序的数组中,可以在O(n)的时间找到中位数。全排序是O(nlogn)时间。
方法:每一轮都使用类似"快排"的方法对无序数组进行分割,但是与快排不同的是:每次分割完之后,只需要对一边进行继续寻找(快排是两边都要)。
所以时间复杂度:O(n)+O(n/2)+O(n/4)+.......+O(1)=O(2n)=O(n)
快排的复杂度:O(n)+O(2*n/2)+O(4*n/4)+......+O(n*1)=logn *O(n)=O(nlogn)
【寻找一个 top-k】
过程同理,每轮划分之后,选择一个partition的时候,
选择top-k所在的区间就行了。
总的时间复杂度也是O(n)
剪绳子
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],...,k[m]。请问k[0]xk[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
方法一:数学函数求导法:针对性规律(特定本题)
result= f(m) = (n/m) ^m,设n为定值,m为自变量,f(m)为乘积结果。
max{ f(m) }= max{ ln f(m) },取对数。
求 ln f(m)= m*( ln n- ln m )最值点的m值,求导并令f(m)'=0,得到m=n/e.
e=2.718,然后因为取整数,所以是拆成一堆2、3;
具体看下:4>>>2*2;5>>>2*3;6>>>3*3 符合分析的结果。
public class Solution { //数学函数求导法,得到:m=n/e (小数的情况下),也就是说尽量拆成一大堆:2、3 public int cutRope(int target) { if(target==2)return 1;//因为题目要求最少拆成2份(m>1) if(target==3)return 2;// int n=target/3; int mod=target%3; int result=1; for(int i=0;i<n;i++)result*=3;//代替乘方运算 //3种情况: if(mod==0); if(mod==1)result=result*4/3;//拆一个3,将3+1=》2+2 if(mod==2)result=result*2; return result; } }//时间复杂度O(N),空间复杂度O(1)
方法二:动态规划(考虑所有情况。效率虽然低,但具有普适性)
public class Solution { public int cutRope(int target) { if(target==2)return 1;//特殊情况:段数>=2,强制分段 if(target==3)return 2; int[] dp=new int[target+1]; dp[1]=1;
dp[2]=2;
dp[3]=3;//在target>=4的前提下,dp数组的1~3对应的值。
for(int i=4;i<=target;i++){ int max=0; for(int j=1;j<=i/2;j++){//注意j范围:1~i/2 max=Math.max(max,dp[j]*dp[i-j]);//动态规划化的重点就是找到【最优子结构的递推公式】 }//Math.max()的使用 dp[i]=max;//存储这个i的最大值(从循环中j个情况中选的) } return dp[target]; } }//时间复杂度O(N^2);空间复杂度O(N)
滑动窗口的最大值(固定窗口宽,窗口里找1个max)
import java.util.ArrayList; import java.util.ArrayDeque; //ArrayList有index->value的对应,而ArrayDeque只有值,没有下标,无法随机查找,只能操作两头,但两头高效。 //ArrayDeque类型,双端队列。(只有两头可以读写,中间不能读写查) public class Solution { public ArrayList<Integer> maxInWindows(int [] num, int size) { ArrayList<Integer> res = new ArrayList<Integer>(); ArrayDeque<Integer> qmax = new ArrayDeque<Integer>(size);//qmax用于存序号i,而不是存num[i]里面的值。 if(num==null || num.length < size || size<=0)return res; for(int i=0; i<=num.length-1; ++i){ //1.右入qmax while(qmax.peekLast()!=null && num[i] > num[qmax.peekLast()])qmax.removeLast(); //【冒泡排序思想】qmax里面的值,左大右小。//去除num[i]左边相邻连续一排较小值。(新比旧大,旧必无用) qmax.addLast(i);//最右边一格,而不一定和原先的连着?? //2.左出qmax if(i-size == qmax.peekFirst()) qmax.removeFirst();//左框最左才会出 //3.res收当前max if(i>=size-1) res.add(num[qmax.peekFirst()]);//最左边一定是当前的最大值,进入结果数组 } return res; } }//时间O(N) 空间O(N) //最粗犷的方法是O(N*size) //因为有while循环,单轮最多O(size)。但整体平均每轮<=O(1),因为数字不够每轮都O(size)规模删除
矩阵中的路径
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。例如:
矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
回溯法
public class Solution { public boolean hasPath (char[][] matrix, String word) { boolean flag[][] = new boolean[matrix.length][matrix[0].length];//flag[][]数组,初始化为false,表示未经过的点 //一次初始化,之后共用 ==>是因为每次试探后,都会复原flag数组 for(int i = 0; i<= matrix.length-1; ++i){ for(int j=0; j<=matrix[0].length-1; ++j){//每行每列的全部格子作为起点,开始尝试 if(dfs(matrix, word, i, j, 0, flag)==true)return true;//如果找到一个,则完成任务+停止尝试,立即返回true } } return false;//全部失败,返回false //单个尝试的失败不会有任何返回 } public boolean dfs(char[][] matrix, String word, int i, int j, int count, boolean flag[][]){ if(0<=i && i<= matrix.length-1 && 0<=j && j<=matrix[0].length-1){//统一拦截==>【剪枝】 if(matrix[i][j] == word.charAt(count) && flag[i][j]==false){//匹配++ ++count;//也可以在后面都用count+1 if(count == word.length())return true;//完整匹配,则主动停止 //全文仅此一处、是true的源头 flag[i][j] = true;//【尝试改flag】(与下文还原flag对应) //下面递归结构类似4叉树的递归: if (( dfs(matrix, word, i+1, j, count, flag) || dfs(matrix, word, i-1, j, count, flag) || dfs(matrix, word, i, j+1, count, flag) || dfs(matrix, word, i, j-1, count, flag) )) return true;//这个return true是带有if的、起到传递true的作用,它不是源头 flag[i][j] = false;//【还原flag】//注意,平时传值都不需要"还原"(如count),而这里需要。 } //说明flag[][]数组,传的是指针(而不是提供副本),递归分支是共用一个的 } return false; } }//时间:O(rows*cols*3^word)//3是因为不能回头、减少一种路 //空间:1)flag空间 O(rows*cols) 2)栈空间O(word) //如果有类似线性匹配的KMP模式串的优化,会快一些
机器人的运动范围
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
回溯法(和上一题类似)
public class Solution {//相邻方格移动 //由于各个位相加(非线性)造成跳步困难,所以用相邻探索的方法 int count = 0; public int movingCount(int threshold, int rows, int cols) { boolean flag[][] = new boolean[rows][cols];//标记方格有没有来过,这里的flag不可逆 dfs(threshold, rows, cols, 0, 0, flag);//从(0,0)开始探索。。 return count; } public void dfs(int threshold, int rows, int cols, int i, int j, boolean flag[][]){ if(0<=i && i<=rows-1 && 0<=j && j<=cols-1){ if(flag[i][j] == false && i/10 + i%10 + j/10 + j%10 <= threshold){//i、j属于[0,99] ++count; flag[i][j] = true;//不用还原 //如果一个块块不符合,它周围就不用再试了 dfs(threshold, rows, cols, i+1, j, flag); dfs(threshold, rows, cols, i-1, j, flag); dfs(threshold, rows, cols, i, j+1, flag); dfs(threshold, rows, cols, i, j-1, flag); } } } }//时间O(rows*cols) 空间O(rows*cols) //感觉修改一下就是迷宫找路
ps:写一些经验
1.不同模块之间适当空行,使结构清晰
2.复杂问题,可以先写几个大思路然后再去填充,比如【先构思】:
private void Mov(int threshold, int rows, int cols, int i,int j,boolean[] flag){ if()return; //【空行】:方便后面填充细节 while(){} while(){} if(sum<=threshold){ result++; flag; //【空行】:使模块间结构清晰 Mov(); M M M } }
之后【填充细节】:
private void Mov(int threshold, int rows, int cols, int i,int j,boolean[] flag){ int index=i*cols+j; if(i<0||j<0||i>=rows||j>=cols||flag[index]==true)return;//此分支不通,不用往下了 int sum=0;int I=i;int J=j; while(I!=0){sum+=I%10;I/=10;} while(J!=0){sum+=J%10;J/=10;} if(sum<=threshold){ result++; flag[index]=true;//设置flag为true,表示已经占用位置,后面不能再走这个格子了。 Mov(threshold,rows,cols,i,j+1,flag); Mov(threshold,rows,cols,i,j-1,flag); Mov(threshold,rows,cols,i+1,j,flag); Mov(threshold,rows,cols,i-1,j,flag);//上下左右试探:在if语句{}里面,因为只有满足sum<=threshold才需要继续探索 } }
注意对比上面两个代码~
终于完成了,哈哈哈!!!
"Microsoft YaHei"