第一章
项目介绍
可按照STAR模型。
S:背景
T:自己完成的任务
A:为了完成任务怎么做的(什么工具什么平台什么技术)
R:成果,实现了怎样的效果,最好能够量化
高质量的代码
边界值的处理,特殊输入,负面输入,最好先写单元测试再写实现。
遇到复杂问题有三个理解的思路:1.画图 2.举例 3.分解
而且还需要关注时间复杂度,空间复杂度等。
提问环节
问相应的人,相应的问题。
基础知识
第一章可能是基础知识吧,都比较简单,书中给出的一些解决思路还是有点妙妙。突然觉得这书用于入门也挺好的
实现Singleton模式
主要就分为懒汉式和饿汉式,懒汉对应延迟加载,不到必要时,不创建,好处在于减少初始化时的负担
数组中的重复的数字
找出一个数组(数字的范围为0~n-1,其中n为长度)中任意一个重复的数字,重复的数字不知道有几个也不知道重复了几次。
比较常规的想法是遍历数组,如果n的范围在0~231 之间可以使用数组记录数字出现的次数,用索引作为数字的标志。如果超出了这个返回可以使用HashSet,一旦某个数字存在过,就可以输出了,而且这种方法可以求出第一个重复的数字(牛客网上对题目的解读是这样的)
书中的给了一个方法,俺觉得有点妙,不过不能保证是第一个重复的数字。
如果没有重复且有序的数组,那么每个数字与数组的索引应该是相对应的,如果出现不对应的那就是出现了重复。
现在有个问题,数组不是有序的。
只需要顺序遍历数组,如果数字与索引对应,则下一个,如果不对应则把数字放到对应的位置,对应位置上的原数字再放到对应的位置,直到某个数字已经在对应的位置为止,或者某个数字没在对应的位置,但是属于它的位置已经被其他数字匹配上了,这时重复的就出现了,这样做找出的数字可能不是第一个重复的数字,不过这是符合题意的(任意)。
private static int func(int[] arr){
if(arr==null){
throw new RuntimeException("无输入");
}
for(int i=0;i<arr.length;i++){
while(arr[i]!=i){
int num = arr[i];
if(arr[num]==num){
return num;
}else{
int temp = num;
arr[i] = arr[num];
arr[num] = num;
}
}
}
throw new RuntimeException("不存在重复值");
}
这种方法会要求修改数组,如果不修改数组呢?(emm时间复杂度有点高…O(nlogN))
可以借助二分查找,因为如果无重复,自然是有0~n-1个数字,那么我们可以算出预设的中间值,遍历数组,算出前半大小的有多少,如果前半多了,说明前半肯定存在重复的数字,那么缩小范围去前半找,反之去后半,直到最后锁定到一个结果,如果数组中存在的该数大于1了,则说明找到啦。
private static int func(int[] arr){
if(arr==null){
throw new RuntimeException("无输入");
}
int left = 1,right = arr.length;
while (left<=right){
int mid = left+right>>1;
int count =0;
for(int i=0;i<arr.length;i++){
if(arr[i]<=mid&&arr[i]>=left)
count++;
}
if(left==right){
if(count>1)
return left;
break;
}
if(count>mid-left+1) {
right = mid;
}else {
left = mid+1;
}
}
throw new RuntimeException("无重复数字");
}
二维数组中的查找
替换空格
将字符串中的空格替换为%20
这题按理说也很简单,用Java的话,转换为StringBuilder/StringBuffer ,将空格替换就好,如果不允许使用这种结构,只允许使用一个字符数组之类的呢?书中给了这样一个场景,需要在原字符串的基础上进行扩容一定的长度,操作完成。(C语言,移动下指针就好)
数组的定义决定了它不能不能扩容,所以一开始要选定数组的长度。
长度就是 原长度+空格的数量×2;
原字符串迁移的话,如果顺着更新,每次遇到空格需要将后续字符串往后迁移2单位,这就会使得时间复杂度为O(n2);
不过逆着更新就不会,每次只用移动对应的。
一个指针指着末尾空闲的位置,另一个指针指着原来字符串的末端,如果是常规字符直接转移即可,遇到空格则用‘%20’进行填充(记得是逆向的哦)
private static String func(String s){
if(s==null||s.length()==0){
return "";
}
char[] cs = s.toCharArray();
int numBlank = 0;
for(int i=0;i<cs.length;i++){
if(cs[i]==' '){
numBlank++;
}
}
int newLen = cs.length+(numBlank<<1);
char[] newC = new char[newLen];
int left = cs.length-1,right = newC.length-1;
while(left>=0){
if(cs[left]==' '){
newC[right--] = '0';
newC[right--] = '2';
newC[right--] = '%';
}else {
newC[right--] = cs[left];
}
left--;
}
return new String(newC);
}
如果合并数组啊字符串啊(那种以一个字符串为基础,然后插着顺序来的),也可以考虑从后往前的来。
从尾到头打印链表
链表对比数组的好处在于,插入和删除的时间复杂度为O(1),但是相应的查询无法根据下标来查询,需要遍历全部,时间复杂度为O(n).
private static void func(ListNode node){
if(node==null){
return;
}
func(node.next);
System.out.println(node.val);
}
重建二叉树
从中序遍历和前序遍历的数组中重组二叉树。
这题还是不错的,实现后,以后编二叉树相关的测试样例方便了很多。
二叉树一般有三种遍历方式(如果层次遍历也算就是四种),前中后。
(图源百度百科,懒得画了)
前序遍历: 优先遍历根节点,然后遍历左节点,最后遍历右节点。比如上图就是FCADBEHGM
中序遍历: 左中右,ACBDFHEMG
后序遍历: 左右中,ABDCHMGEF
这题中根据前序和中序还原,根据前序,可以知道某一层的根节点,然后在中序遍历中,它之左的都是左子树的节点,之右的都是右子树的节点。
也就是说,用前序节点区分树的左右,再以该节点在中序的位置来限定下一个右子树根节点的位置。
假设现在中序的限定范围为left~right,前序对应的位置在其中的inx位置,inx在中序的位置为mid,那么left~mid-1都是左子树,mid+1 ~ right都是右子树。
左子树的总大小为mid-1-left+1,那么前序里右子树的根节点位置在inx+1+mid-1-left+1.左子树倒是简单,就是inx+1.
好了这些都理解了就可以写代码了。
private static TreeNode func(int[] pre,int[] in,int inx,int left,int right){
if(left>right||left<0||right<0||left>=pre.length||right>=pre.length)
return null;
if(inx>=in.length){
throw new RuntimeException("数据不匹配");
}
int mid = find(pre[inx],in);
if(mid==-1)
throw new RuntimeException("数据不匹配");
TreeNode root = new TreeNode(pre[inx]);
if(mid<=right||mid>=left){
root.left =func(pre,in,inx+1,left,mid-1);
root.right = func(pre,in,inx+mid-left+1,mid+1,right);
}
return root;
}
private static int find(int pre,int[] in){
return IntStream.range(0,in.length).filter(i->pre==in[i]).findAny().orElse(-1);
}
二叉树的下一个节点
给定一棵二叉树,和其中的一个节点,中序遍历下,该节点的下一个节点是啥。这个二叉树除了左右指针还多了一个指向父节点的指针。
还是从中序的定义出发,中序是左中右。
一个节点有右儿子,则下一个就是它的右边子树的最左节点。
一个节点莫得右儿子,1.自己是属于左子树那就找它的直接父节点,2.如果自己属于右儿子,这种情况麻烦,需要找到一个至少为左子树的祖先,再取该祖先的父节点;如果找不到那就是没得了.
public TreeLinkNode GetNext(TreeLinkNode pNode)
{
if(pNode==null)
return null;
TreeLinkNode ans;
if(pNode.right!=null){
TreeLinkNode tmpR = pNode.right;
while(tmpR.left!=null){
tmpR = tmpR.left;
}
ans = tmpR;
}else if(pNode.next!=null&&pNode.next.right == pNode){
TreeLinkNode father = pNode.next;
while(father!=null&&father.next!=null&&father.next.right==father){
father = father.next;
}
ans = father==null?null:father.next;
}else
ans = pNode.next;
return ans;
}
用两个栈实现队列
栈的特征是后进先出,队列是先进先出。
直到了这个特征,用一个栈用于入队,一个用于出队,第一次出队时,如果两个栈都为空抛出异常,如果不为空,如果出队的栈是空的,则将入队的栈里的元素压入出队的栈,这一就实现“逆序”,与队列原本顺序一致了,如果出队的栈不是空的,直接出队(栈)即可。
static private class Queue<T>{
java.util.Stack<T> sk1 = new java.util.Stack<>();
java.util.Stack<T> sk2 = new java.util.Stack<>();
int size;
public void offer(T i){
sk1.push(i);
size++;
}
public boolean isEmpty(){
return size==0;
}
public T poll(){
if(isEmpty())
throw new RuntimeException("队列为空");
if(sk2.isEmpty()){
while(!sk1.isEmpty()){
sk2.push(sk1.pop());
}
}
size--;
return sk2.pop();
}
}
那两个栈实现队列呢?
这个可就比俩栈实现队列麻烦了,只能用一个队列用于临时队列了,当需要出栈时,将队列里除了最后一个全部入另一个队,只剩一个,再将这个出队,然后再交换这俩队列的关系。
static private class Stack<T>{
java.util.Queue<T> qu1 = new LinkedList<>();
java.util.Queue<T> qu2 = new LinkedList<>();
int size;
public void push(T t){
qu1.offer(t);
size++;
}
public boolean isEmpty(){
return size==0;
}
public T pop(){
if(isEmpty())
throw new RuntimeException("栈为空");
while(qu1.size()>1){
qu2.offer(qu1.poll());
}
T ans = qu1.poll();
size--;
java.util.Queue<T> temp = qu1;
qu1 = qu2;
qu2 = temp;
return ans;
}
}
斐波那契数列
跳楼梯与变态跳楼梯
旋转数组的最小数字
一个排序数组旋转了,就是原本1,2,3,4,5->4,5,1,2,3…之类的,不存在重复的数字,找出其中的最小数字。
使用二分法,左指针指向最左端,右指针指向右端,取他俩中间的那个值,
如果那个值大于了左指针,
有两种可能,要么处于旋转的那段,要么数组根本没有旋转,第一种最小值肯定再右端,第二种为0号位置,但是由于如果多加判断的话会显得逻辑比较繁琐(也没多大繁琐的,加个左端和右端的判断即可,不过如果不是那种唯一的顺序情况,其他情况下,进行都不必要的额外判断就会浪费些时间)。
如果中间值小于左指针则说明在最小值可能是当前值也可能在左边。
public static int findMin(int[] array){
if(array==null||array.length<1)
return -1;
int left = 0,right = array.length-1;
while (left<right){
int mid = left+right>>1;
if(array[mid]<array[0]){
right = mid;
}else {
left = mid+1;
}
}
return Math.min(array[left],array[0]);
}
}
矩阵中的路径
给定一个字符矩阵,再给定一个字符串,在矩阵中是否存在满足字符串顺序与字符的路径,路径无环,合法的方向为上下左右。
这是一道回溯的题,不知道存不存在,以所有点作为可能的开始,然后从四个分别探索,如果从某个点开始匹配不上则回退一步从另一个方向探索,直到四个方向都不对,再回退一步,直到退完…或者找到了。
由于不能重复走,所以需要用一个标志来标记在当前路径下某个点是否走过。
遇到这种题可以画一画递归树。
private static boolean find(char[][] m,String target){
if(m==null||m.length==0)
return target.length()==0;
for(int i=0;i<m.length;i++){
for(int j=0;j<m[0].length;j++){
if(find(m,target,i,j,0,new boolean[m.length][m[0].length]))
return true;
}
}
return false;
}
private static boolean find(char[][] m, String target,int x,int y,int inx,boolean[][] flag) {
if(x<0||x>=m.length||y<0||y>=m[0].length||flag[x][y]||inx==target.length())
return inx==target.length();
if(m[x][y]!=target.charAt(inx))
return false;
flag[x][y] = true;
inx++;
return find(m,target,x+1,y,inx,flag)||find(m,target,x-1,y,inx,flag)||find(m,target,x,y-1,inx,flag)||find(m,target,x,y+1,inx,flag);
}
机器人的运动范围
地上有m×n的方格,机器人从(0,0)出发,可以沿着上下左右四个方向运动,但是它不能去格子坐标的数位之和大于k的位置。问它能够到达多少个格子。
还是上一题那么回事呗,遇到不能走的回退,遇到能走的,走,然后格子数+1,不过不能走重复的位置。
public int movingCount(int threshold, int rows, int cols) {
if(threshold<=0)
return 0;
return moveCount(rows,cols,0,0,threshold,new boolean[rows*cols]);
}
private static int moveCount(int x, int y, int i, int j, int limit,boolean[] flag) {
if(limit<sum(i)+sum(j)||i<0||i>=x||j<0||j>=y||flag[i*y+j])
return 0;
flag[i*y+j] = true;
return 1+moveCount(x,y,i-1,j,limit,flag)+moveCount(x,y,i+1,j,limit,flag)+moveCount(x,y,i,j+1,limit,flag)+moveCount(x,y,i,j-1,limit,flag);
}
private static int sum(int i){
int count =0;
while (i>0){
count+=i%10;
i/=10;
}
return count;
}
剪绳子
有一段绳子,长度为n可剪成任意段,求段之间的乘积最大的值。
这题能用动态规划解,也可用贪心解决,动态规划本质我觉得是普适版的贪心。
动态规划能否使用有俩条件,1.划分子问题,子问题的累积就是最终结果,2.这些重叠子问题的最终结果的来源是否会影响到后面的结果。
那么贪心就是在能够使用动态规划的基础上,每次采用的策略都是一样的,就可以使用贪心,贪心会比动态规划了来得快。
贴一下俺与大佬的对话,俺觉得对俺很有启示
回到本题,如果使用动态规划,重叠子问题就是,一段绳子可以拆分为(1~n-1)的长度仍选,然后另外两段又能够拆分,拆分到为1的时候就不拆了。再选择最大的情况保存,上一次拆分再选择能选择的最大的那个保存。
private static int max(int target) {
if(target<2)
return 0;
int[] dp = new int[target];
dp[0] = 1;
dp[1] = 2;
dp[2] = 3;
for(int i=4;i<=target;i++){
for(int j=1;j<=i>>1;j++){
dp[i-1] =Math.max(dp[i-1],dp[i-j-1]*dp[j-1]);
}
}
return dp[target-1];
}
贪心的话,尽可能的多拆分为3的段数,如果剩下为1,则与3合并,如果剩下为2,则不作处理,最后相乘就行了。
private static int maxT(int target){
if(target<2)
return 0;
int n = target/3;
if(target-n*3==1)
n-=1;
int n2 = target-n*3>>1;
return myPow(3,n)*myPow(2,n2);
}
private static int myPow(int d,int e){
if(e==0)
return 1;
if(e==1)
return d;
int tmp = 1;
while (e>0){
if((e&1)==1)
tmp *= d;
d*=d;
e>>=1;
}
return tmp;
}
如果拆分的段数限定的话也同贪心的道理,尽可能拆分得平均,如果段数大于每份拆分为3份的段数,则等同于上面那种。
二进制中的1
给定一个int类型的数字,算出其中包含的1的数量。
注意这题数字是可为负数的,负数也就是32位为1,然后对整数的情况下取补码(取反+1)。
那么可以这样用1,一直向右位移,与目标数字求与,如果为1则保存,那个数字位移到第32位位置(该数字就小于0了),如果原数字是负数,则再在结果上+1;
public class Solution {
public int NumberOf1(int n) {
int f =1,ans =0;
while (f>0){
if((n&f)!=0)
ans++;
f=f<<1;
}
return n<0?ans+1:ans;
}
}
也可以用另一种方法,一个数字自己与自己求与肯定是不会变的,那么它-1,就相当于减去了最低位,再求与最低位就被抵消掉,如果此时该数不为0,则说明至少还有一个位为1,直到结果为1为止。
private static int count(int n){
int ans =0;
while (n!=0){
ans++;
n = (n-1)&n;
}
return ans;
}