在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
【思路】
左上最小、右下最大。右上or左下出发,快速切边查找;每一步都只有一个选择=》不产生分支
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)
1) 原版(给StringBuffer):
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
【思路】
如果给StringBuffer就直接在上面改;从后往前,进行扩张。
如果给String则需要新开辟空间,然后从前往后即可。
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上面改
2) 新版(给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)递归方法(系统栈):类似于树只有单分支,持续向下到末端后,后续遍历往上
2)用栈反转:注意,从尾到头打印值,可能只是反转值,而没有反转链表。
3)头插法直接存到ArrayList res,这个效率最低,所以说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());//这里只是反转了val, 如果要反转链表可以新建Node
}
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)
//时间效率最低,所以说ArrayList尽量不要用头插法
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
【思路】
前序+中序=》重建二叉树
先序pre[0]做根切割中序
/**
* 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)
用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
【思路】
如果不控制好栈1栈2的数据流动,可能会造成数据顺序错乱
只有栈2排空了,才会由1到2,必须一次性全部
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出一个)
}
}
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{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)
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。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];
}
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)
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?
比如n=3时,23的矩形块有3种覆盖方法:
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个块块
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
方法一:按位与
public class Solution {
public int NumberOf1(int n) {
//可以直接拿int类型,当做二进制来进行位运算 (IDEA上亲测可用)
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
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
保证base和exponent不同时为0
public class Solution {
public double Power(double base, int exponent) {
if(exponent == 0)return 1;
double res = 1.0;
if(exponent <0){
base = 1.0/base;
exponent *=(-1);
}
for(int i=1; i<=exponent; ++i)res*=base;
return res;
}
}
//时间 O(exponent)
//空间 O(1)
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
【思路】
1)若是保证顺序:需要新开辟空间
2)若是只需要区分奇偶数,就直接在原数组上用双指针 (类似快排:一个条件划分两块)
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)
输入一个链表,输出一个链表,该输出链表包含原链表中从倒数第k个结点至尾节点的全部节点。
如果该链表长度小于k,请返回一个长度为 0 的链表。
1)标准的 双指针法
public class Solution {
public ListNode FindKthToTail (ListNode pHead, int k) {
ListNode pre = pHead;//head是第一个节点
ListNode res = pHead;
while(--k >= 0){
if(pre== null)return null;//倒数第k的k值大于链表总长度
pre=pre.next;
}
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】//两个指针可以是pre/cur,但多了用1/2/3比较好(清晰)
//反转时候最怕的就是单链断掉,所以这里用几个节点指针来缓存“断链操作”处的信息
ListNode p1=null;//第一次使head指向null
ListNode p2=head;//【p2是head】
ListNode p3=null;
while(p2!=null){
//选p2是因为p2第一次作为head、正好去遍历全部单链节点
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 vHead = new ListNode(Integer.MIN_VALUE);//建立虚拟头结点 //建立的时候不能用null,必须实例化
ListNode p=vHead;
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 vHead.next;//虚拟头结点一直在那不动,返回它的下一个即为第一个真实节点
}
}
输入两棵二叉树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)
leetcode变种版本:
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3
/
4 5
/
1 2
给定的树 B:
4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B==null)return false;
return tryTree(A,B);
}
public boolean tryTree(TreeNode A, TreeNode B){
if(A!=null){
if(judge(A,B)==true)return true;
if(tryTree(A.left, B)==true || tryTree(A.right, B)==true)return true;//这里没有那么简洁,但是可读性好。
}
return false;
}
public boolean judge(TreeNode A, TreeNode B){
if(B!=null){
if(A==null || B.val != A.val)return false;
return (judge(A.left,B.left)==true)&&(judge(A.right,B.right)==true);
}
return true;
}
}
操作给定的二叉树,将其变换为源二叉树的镜像。
public class Solution {
//总体思想:递归遍历+交换
public TreeNode Mirror (TreeNode pRoot) {
if(pRoot!=null){
//递归终止条件:null时不再向下
TreeNode temp = pRoot