剑指offer大总结
字符串
字符串流中第一个只出现一次的字符
题目描述:
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
解题思路1:
(1)使用一个HashMap< Character,Integer>来存放字符:个数;同时让一个全局变量String来保存当前流的顺序,str+=ch;
(2)遍历这个HashMap,如果发现这个HashMap中存在个数为1的字符,使用indexOf()方法查找到这个字符的索引,并返回这个字符。
(3)注意,map是无序的存储,所有需要在遍历的过程中判断并获取所有keySet中索引最小的字符。
import java.util.Map;
import java.util.TreeMap;
public class Solution {
//Insert one char from stringstream
String str="";
Map<Character,Integer> map=new TreeMap<Character,Integer>();
public void Insert(char ch){
if(map.containsKey(ch)){
map.put(ch,map.get(ch)+1);
}else{
map.put(ch,1);
}
str+=ch;
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce(){
int index=Integer.MAX_VALUE;
char result='#';
for(Character c: map.keySet()){
if(map.get(c) == 1){
if(str.indexOf(c) < index){//由于map是无序的,所以需要额外的获取索引最小的字符
index = str.indexOf(c);
result = str.charAt(index);
}
}
}
return result;
}
}
解题思路2
(1)利用一个哈希数组,用于计数。数组的长度为128,因为ASCII也就128个字符,因此这种解法只针对英文字符串。
(2)队列用于缓存输入流,只有新字符才入队列(无论元素是否重复,保证队列中只存在一个元素值),哈希数组记录字符出现的次数,出队列时,判断队列队首的字符是否重复(哈希数组中的个数>1),如果重复,直接出队列,否则,返回这个字符。
(3)一直循环上述(1)和(2)步骤。直到队列为空。
用到的几个关键API
Character c: c.charValue();
import java.util.*;
public class Solution {
//Insert one char from stringstream
int[] charCount=new int[128];
Queue<Character> queue=new LinkedList<Character>();
public void Insert(char ch){
if(charCount[ch]==0){
queue.add(ch);//队列中的重复的元素中只存在一个;
}
charCount[ch]++;//计数加一
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce(){
Character ch=null;
while(( ch=queue.peek())!=null){
char c=ch.charValue();
if(charCount[c]==1){
//只有一个
return c;
}else{
//出队列
queue.poll();//重复的元素去掉,再根据队列的特性,就可以按顺序得到第一个不重复的值(队首)
}
}
return '#';
}
}
判断字符串是否表示为数值
题目描述:
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
正例 "+100","5e2","-123","3.1416"和"-1E-16"都表示数值。
反例 "12e","1a3.14","1.2.3","+-5"和"12e+4.3"
解题思路:
遍历字符串中的每一个字符,针对+ - E e 数字出现的特殊位置,排除反例
(1)当前字符为:+ 或者 - 时,则该字符首次出现必须在str[0]位置,第二次出现,则必须位于E或者e之后;
(2)当前字符为:. 时,只能出现一次,E后面不能出现小数点
(3)当前字符为:E或者e时,不能同时存在两个E,E/e后面一定要有数字;
(4)当前字符不能出现以上和0~9之外的数字。
【技巧】可以定义三个状态变量用于记录 +/- . E/e 的状态。
public class Solution {
public boolean isNumeric(char[] str) {
boolean hasSign=false;//是否出现过+ 和 -
boolean hasDecimal=false;//是否出现过小数点;
boolean hasE=false;//是否出现过E e
for(int i=0;i<str.length;i++){
//分三种大情况:
//1.出现+ -
//2.出现 .
//3.出现 E e
//4.数字
if(str[i]=='+' || str[i]=='-'){
//+号如果不是是第一次出现,则它必须出现在E或者e的后面
if(hasSign && str[i-1]!='E' && str[i-1]!='e'){
return false;
}
//第一次出现,必须出现在首位
if(!hasSign && i>0 && str[i-1]!='E' && str[i-1]!='e'){
return false;
}
hasSign=true;
}else if(str[i]=='.'){
//不能出现两个.
if(hasE || hasDecimal){
return false;
}
hasDecimal=true;
}else if(str[i]=='E' || str[i]=='e'){
//不能重复出现 E 或者 e
if(hasE){
return false;
}
//E 不能出现在最后一位,即后面要接数字或者+ -
if(i==str.length-1){
return false;
}
hasE=true;
}else if(str[i]<'0' || str[i]>'9'){
return false;
}
}
return true;
}
}
字符串左旋K位操作
题目描述:
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).(从0开始计数)
方法一:
利用一个队列,将字符串中的字符入队,从队首出n个字符放入队尾。
import java.util.*;
public class Solution {
public String LeftRotateString(String str,int n) {
if(str.length()==0){
return "";
}
Queue<Character> queue=new LinkedList<Character>();
StringBuilder res=new StringBuilder();
for(int i=0;i<str.length();i++){
Character ch=str.charAt(i);
queue.offer(ch);
}
int count=0;
while(count++<n){
queue.offer(queue.poll());
}
while(!queue.isEmpty()){
res.append(queue.poll());
}
return res.toString();
}
}
方法2:
import java.util.*;
public class Solution {
public String LeftRotateString(String str,int n) {
int len = str.length();
if(len == 0) return "";
n = n % len;
str += str;
return str.substring(n, n+len);
}
}
正则表达式匹配问题
题目描述:
请实现一个函数用来匹配包括'.'和''的正则表达式。模式中的字符'.'表示任意一个字符,而''表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
解题思路:
这道题如果硬刚,情况有很多种,可以使用递归的思想将来逐个字符比较,递归的核心就是比较当前字符是否匹配,在保证当前字符匹配的条件下,进行下一个字符的匹配,由于‘*’和‘.’出现位置的不确定性,我们需要分类讨论:
(1)判断当前first=str[ i] = =pattern[ j] || pattern[ j]=='.' , 所以则后面res=fist && str[ i+1:]?pattern[ j+1:];
(2)在判断str[ i+1:]和pattern[ j+1]的时候
(2.1)如果pattern[ j+1]'*' && j+1< pattern.length * 匹配0个字符,则要继续比较pattern[ j+2]str[ i]
(2.2) 重复至少一个的时候,在保证first匹配的情况下,还需要保证pattern[j]==str[ i+1];
(3)在2不成立的情况下(不含有*和.),需要判断first && str[ i+]==pattern[ j+1]
class Solution {
boolean[][] dp;
public boolean isMatch(String s, String p) {
dp=new boolean[s.length()+1][p.length()+1];
return match(0,0,s,p);
}
/**
*i: text的索引
*j: pattern的索引
**/
boolean match(int i,int j,String text,String pattern){
boolean res;//保存字符窜的匹配结果
if(j==pattern.length()){//递归结束条件
res = i==text.length();
}else{
//如果当前pattern中的字符为'.',则当前字符的匹配一定为true
//否则,则检索text和pattern中对应的字符是否相等
boolean curMatch=i<text.length()&&(text.charAt(i)==pattern.charAt(j) || pattern.charAt(j)=='.');
//分两种情况处理:pattern[j+1]是否包含'*';
if(j+1<pattern.length() && pattern.charAt(j+1)=='*'){
//当包含的时候又分两种情况
//情况1:'*',重复0个前一字符,不考虑当前字符和前一字符,即比较text[i]?=pattern[j+2];
//情况2:'*',重复多个字符,此时判断curMatch和text[i+1]?=pattern[j];如果失配,则无论重多少
//个字符串都会失效
res = match(i, j + 2, text, pattern) || curMatch && match(i + 1, j, text, pattern);
}else{
//不包含时候,直接匹配下一位
res = curMatch && match(i + 1, j + 1, text, pattern);
}
}
dp[i][j]=res?true:false;//维护一张动态规划表
return res;
}
}
正则表达式2:
问题描述:在计算机中,通配符一种特殊语法,广泛应用于文件搜索、数据库、正则表达式等领域。现要求各位实现字符串通配符的算法。
要求:
实现如下2个通配符:
*:匹配0个或以上的字符(字符由英文字母和数字0-9组成,不区分大小写。下同)
?:匹配1个字符
与题解题思路基本一致,注意这题不是重复前面的字符,而是匹配任意字符
import java.io.*;
public class Main{
public static void main(String[] args) throws IOException{
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
String str="";
while((str=br.readLine())!=null){
String text=br.readLine();
boolean res=match(0,0,text,str);
if(res){
System.out.println("true");
}else{
System.out.println("false");
}
}
}
public static boolean match(int i,int j,String text,String pattern){
boolean res;//保存字符窜的匹配结果
if(j==pattern.length()){//递归结束条件
res = i==text.length();
}else{
//如果当前pattern中的字符为'?',则当前字符的匹配一定为true
//否则,则检索text和pattern中对应的字符是否相等
boolean curMatch=i<text.length()&&(text.charAt(i)==pattern.charAt(j) || pattern.charAt(j)=='?');
//分两种情况处理:pattern[j+1]是否包含'*';
if(j+1<pattern.length() && pattern.charAt(j+1)=='*'){
//当包含的时候又分两种情况
//情况1:'*',匹配0个字符,text[i+1] 和pattern[j+2];
//情况2:'*',匹配多个字符,和text[i+2]?=pattern[j+2];如果失配,则无论重多少
//个字符串都会失效
res = curMatch && (match(i + 2, j+2, text, pattern) || match(i + 1, j+2, text, pattern)) ;
}else{
//不包含时候,直接匹配下一位
res = curMatch && match(i + 1, j + 1, text, pattern);
}
}
return res;
}
}
字符串转整数
题目描述:
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0
解题思路:
(1)+ 或者 - 要么没有,有则一定出现在str[0];
(2)从str[1:]开始,检查每一个字符,res+=res*Math.pow(1,str.length-1-i);
(3)最后需要判断,如果res为整数,则应该<=Integer.MAX_VALUE;如果是负数,则需要>=Integer.MIN_VALUE
(4)注意一个特判断,str为null或者"".equals(str)时,返回0;
import java.util.*;
public class Solution {
public int StrToInt(String str) {
if(str.length()==0 || str.equals("")){
return 0;
}
boolean isPostive=true;
int res=0;
for(int i=0;i<str.length();i++){
char ch=str.charAt(i);
if(i==0){
if(ch=='+'){
isPostive=true;
}else if(ch=='-'){
isPostive=false;
}else if(!isNumric(ch)){
return 0;
}else{
res+=(ch-'0')*Math.pow(10,str.length()-i-1);
}
}else{
if(isNumric(ch)){
res+=(ch-'0')*Math.pow(10,str.length()-i-1);
}else{
return 0;
}
}
}
if(isPostive && res<=Integer.MAX_VALUE){
return res;
}
if(!isPostive && -1*res>=Integer.MIN_VALUE){
return -1*res;
}
return 0;
}
private boolean isNumric(char c){
if(c<'0' || c>'9'){
return false;
}
return true;
}
}
扑克牌顺子
问题描述:
本质描述:0可以代替任意数字,随机给定5个数,他们可以组成一个严格递增的序列,其中0的个数不会超过4;
解题思路
(1)将数组从小到大排序
(2)计算出0的个数 zeroNums
(3)从非零的数开始比较,相邻两数之间不能相同;相邻两数之间的差值减1要小于等于zeroNumm,这样才能保证0可以补全之间的空隙,同时剩余的0又可以继续去补下一轮,注意,此时i应该是< numbers.length-1;这样才能保证i+1不会超出索 引。
import java.util.Arrays;
public class Solution {
public boolean isContinuous(int [] numbers) {
//特判
if(numbers.length!=5){
return false;
}
//一定要排序
Arrays.sort(numbers);
int zeroNum=0;
//计算0的个数
int i=0;
while(i<numbers.length && numbers[i]==0){
zeroNum++;
i++;
}
//此时i指向第一个不为0的数
for(;i<numbers.length-1;i++){
//不能出现对子
if(numbers[i]==numbers[i+1]){
return false;
}
//前后两值需要相差1,可以用0来补充。
if(numbers[i]+zeroNum>=numbers[i+1]-1){
//numbers[i+1]与numbers[i]+1 的差值不能超过zeroNum,否则补不回来
zeroNum-=numbers[i+1]-1-numbers[i];//计算剩余的zeroNum
}else{
return false;
}
}
if(i==numbers.length-1){
return true;
}else{
return false;
}
}
}
翻转一句话中的单词
题目描述:
例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”
特别注意一个细节
" "和" "是不同的字符串,所以用str.trim().equal(""),让其原样输出
public class Solution {
public String ReverseSentence(String str) {
StringBuilder res=new StringBuilder();
if(str==null ){
return null;
}
if(str.trim().equals(" ")){
return str;
}
String[] strs= str.split(" ");
for(int i=strs.length-1;i>=0;i--){
if(i==0){
res.append(strs[i]);
}else{
res.append(strs[i]);
res.append(" ");
}
}
return res.toString();
}
}
旋转字符串问题(难)
问题描述:
一个字符串可以分解为多种二叉树。如果str长度为1,认为不可分解;如果str长度为N(N>1),左半部分长度可以为1~N-1,右半部分为剩下的长度,然后你可以交换左右两部分。并且左右部分可以按照同样的逻辑,继续分解。每一个形成的字符串都可以是原字符串的旋变字符串。现在给你两个字符串str1和str2,判断str2是否为str1的旋变字符串。
解题思路:
直接看图
参考代码
import java.io.*;
public class Main{
public static void main(String[] args) throws IOException{
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
String s1=br.readLine();
String s2=br.readLine();
if(helper(s1,s2)){
System.out.print("YES");
} else{
System.out.print("NO");
}
}
//判断两个字符串中字符种类是及其长度是否相同
private static boolean isSameChar(char[] str1,char[] str2){
if (str1.length != str2.length) {
return false;
}
int[] map = new int[256];
for (int i = 0; i < str1.length; i++) {
map[str1[i]]++;
}
for (int i = 0; i < str2.length; i++) {
if (--map[str2[i]] < 0) {
return false;
}
}
return true;
}
//str1和str2的种类和长度都相同
//判断是否互为旋变字符串
//str1[i,i+size)和str[j,j+size)是否为旋变字符串
private static boolean isScrambel(char[] str1,char[] str2,int L1,int L2,int size){
if (size == 1) {
return str1[L1] == str2[L2];
}
// 枚举所有情况,有一种满足就返回true
for (int leftPart = 1; leftPart < size; leftPart++) {
if((isScrambel(str1, str2, L1, L2, leftPart) && isScrambel(str1, str2, L1 + leftPart, L2 + leftPart, size - leftPart)) || (isScrambel(str1, str2, L1, L2 + size - leftPart, leftPart) && isScrambel(str1, str2, L1 + leftPart, L2, size - leftPart))) {
return true;
}
}
return false;
}
private static boolean isScramble2(char[] s1, char[] s2) {
int N = s1.length;
boolean[][][] dp = new boolean[N][N][N + 1];
// 初始可以填充位置
for (int L1 = 0; L1 < N; L1++) {
for (int L2 = 0; L2 < N; L2++) {
dp[L1][L2][1] = s1[L1] == s2[L2];
}
}
// 其余位置
// 第一层for循环是依次填充size=2、3、...、N层,每一层对应一个二维平面
for (int size = 2; size <= N; size++) {
// 第二三层for循环就是填充这个二维平面
for (int L1 = 0; L1 <= N - size; L1++) {
for (int L2 = 0; L2 <= N - size; L2++) {
// 第四层for循环对应递归函数内容
for (int leftPart = 1; leftPart < size; leftPart++) {
if((dp[L1][L2][leftPart] && dp[L1 + leftPart][L2 + leftPart][size - leftPart]) || (dp[L1][L2 + size - leftPart][leftPart] && dp[L1 + leftPart][L2][size - leftPart])) {
dp[L1][L2][size] = true;
break;
}
}
}
}
}
return dp[0][0][N];
}
private static boolean helper(String s1,String s2){
if ((s1 == null && s2 != null) || (s1 != null && s2 == null)) {
return false;
}
if (s1 == null && s2 == null) {
return true;
}
if (s1.equals(s2)) {
return true;
}
char[] str1 = s1.toCharArray();
char[] str2 = s2.toCharArray();
if(!isSameChar(str1,str2)){
return false;
}
return isScramble2(str1,str2);
}
}
上述步骤的时间复杂度高,可以将上述函数改成动态规划的形式,设置一个三维的动态规划表dp[ L1 ][L2][ len+1],表示str1的L1位置开始到len位置截取,str2的L2位置开始到冷len位置截取;并同时要掌握一个判断两个字符串是否等同的小技巧。
数组问题
构建乘积数组
题目描述:
给定一个数组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];)
题目分析:
观察发现,B[i]的结果为A[0:length-1]中除去A[i]元素的乘积。
public class Solution {
public int[] multiply(int[] A) {
int[] B=new int[A.length];
for(int i=0;i<B.length;i++){
B[i]=1;
}
for(int i=0;i<B.length;i++){
for(int j=0;j<A.length;j++){
if(i==j){
continue;
}
B[i]*=A[j];
}
}
return B;
}
}
数组中重复的数字
题目描述:
长度为n的数组中元素大小均为0~n-1,判断该元素是否存在重复的元素,并且输出这个元素到一个数组duplication[ 0 ]
解题思路:
设置一个count[]数组专门用于记录每一个元素的数量,然后遍历这个count数组,如果找到count [ number[ i ]]>=2,则将该元素number[i]赋值给duplication[0],这其实就是java中使用数组来模拟输出元素。
public class Solution {
// Parameters:
// numbers: 输入数组
// length: 输入数组的长度
// duplication: 输出数组,用于记录找到的任意一个重复的数字
// Return value: 找了则为True,否则为False
public boolean duplicate(int numbers[],int length,int [] duplication) {
if(length==0){
return false;
}
int[] count=new int[length];
for(int i=0;i<length;i++){
count[numbers[i]]++;
}
for(int i=0;i<count.length;i++){
if(count[numbers[i]]>=2 ){
duplication[0]=numbers[i];
return true;
}
}
return false;
}
}
和为sum的两个数字
题意描述:
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
解题思路:
由于数组已经是有序的,设置left指针指向数组头,right指针指向数组尾,两者相向夹逼,在夹逼的过程中,会出现以下三种情况。
(1)arr[left ]+arr[right ]==sum,表明找打,且两数相差越大,乘积越小,所以只要发现,则这个值必然符合乘积最小的要求。
(2)arr[left ]+arr[right ]>sum,此时需要增到其中一个加数,只能是left++(因为数组递增);
(3) arr[left ]+arr[right ]< sum,需要减少一个加数,只能是right--;
//按照上面的分析,这里的代码可以在找到符合要求的left和right直接添加进返回集合中,但是,这里给出一个判断的步骤。仅供参考。
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
ArrayList<Integer> res=new ArrayList<Integer>();
if(array.length<2){
return res;
}
int left=0;
int right=array.length-1;
int s=0;
int minNum1=-1;//记录最小乘积对应的left;
int minNum2=-1;//记录最小乘积对应的right;
int min=Integer.MAX_VALUE;//初始最小值。
while(left<right){
s=array[left]+array[right];
if(s<sum){
left++;
}else if(s>sum){
right--;
}else{
if(array[left]*array[right]<=min){
min=array[left]*array[right];
minNum1=left;
minNum2=right;
}
left++;//继续寻找下一对
right--;
}
}
//只有最小的才进入结果集合。
if(minNum1>=0 && minNum2>=0){
res.add(array[minNum1]);
res.add(array[minNum2]);
}
return res;
}
}
数组中只有唯一一个元素重复
题目描述:
数组大小为n+1,其元素为1~n,其中只有一个元素重复了,其他元素均出现一次,请设计算法找出这个元素。
解题思路:
使用异或操作符来实现。它有三个性质
(1)n^n=0
(2)n^0=n
(3)满足交换律,abc=a(bc)
根据上面三个性质我们可以设计如下算法,假设n=1000,即在{1,2,3,...,n,n,...1000}构成的数组中找到n。
- 设T=123...nn...1000=123...^1000(去除了重复的元素n)
- 设Q=123....n...^1000(只含有一个n)
- 根据运算规则,T^Q=n;
根据上述公式我们可以编写如下代码
class Solution{
public int finDifferent(int[] arr){
int T=0;
int Q=0;
//T=1^2^3^...^n^n^...^1000
for(int i=0;i<arr.length;i++){
T ^= arr[i];
}
//Q=1^2^3^...^n^...^1000
for(int i=1;i<arr.length;i++){
Q^=i;
}
return T^Q;
}
}
数组中只出现一次的两个数字
题目描述:
一个数组中,只有两个数字没有重复,其余的数字均重复一次,请设计算法找出这两个没有重复的数字。
解题思路:
这道题解题的思路和上一题的核心思想一样。我们以number=[1,2,2,3,3,4 ]为例进行说明。
- 设T=122334=14
- 若能number分成两个子数组a=[1, 3,3]和b=[4,2, 2],那么我们可以通过Ta=14(122)=4,Tb=14(4,3,3)=1
- 关键在于如何将number划分为a和b两个数组?
由于T=14(两个数不相同,则T必然不为0)转化成二进制后,至少有一个位(N)是1,并且1和4的二进制的第N位必然是不同的(根据运算规则),那么可以根据二进制的N位是否为1来将数组划分为两组。上个案例中划分后的可能结果是:a=[1, 3,3] b=[2, 4,4],最后133=1 244=4,获得最后的结果。
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
int T=0;
//所有数^的结果
for(int i=0;i<array.length;i++){
T^=array[i];
}
//找到T中的二进制中为1的位
int index=findOneBit(T);
//遍历数组,并分组
num1[0]=0;
num2[0]=0;//0^n=n
for(int i=0;i<array.length;i++){
if(isBitOne(array[i],index)){
//a组,和T进行迭代^运算
num1[0]^=array[i];
}else{
num2[0]^=array[i];
}
}
}
//找到num的二进制的位为1的索引,
//如:00100 返回 2
private int findOneBit(int num){
int index=0;
while(( num & 0x1)==0){
num=num >> 1;
index++;
}
return index;
}
//判断num的第index位是否为1
private boolean isBitOne(int num,int index){
//将num左移index位,然后& 0x1
num=num >> index;
return (num & 0x1)==1?true:false;
}
}
有序数组统计个数
题目描述:
统计有序数组中指定值的个数,要求时间复杂位logn
解题思路:
看到logn,条件反射地想到二分法,只是要做一个小小的改进,在找到k==array[mid ]之后,不能立即返回,应该继续以这个点分别向左和向右继续寻找,当前count+1,无论向左还是向右,只要找到了count++
(1)向左的边界不能< 0,向右继续寻找的边界不能>arr.length-1
public class Solution {
public int GetNumberOfK(int [] array , int k) {
int count=0;
int l=0;
int r=array.length-1;
while(l<=r){
int mid=(l+r)/2;
if(array[mid]>k){
//左半区域
r=mid-1;
}else if(array[mid]<k){
l=mid+1;
}else{
//当前mid符合,count+1;
count++;
//向mid的继续向左边扫描
int tmp=mid-1;
while(true){
if(tmp<0 || k!=array[tmp]){
break;
}
count++;
tmp--;
}
//向右边继续扫描寻找
tmp=mid+1;
while(true){
if(tmp>array.length-1 || k!=array[tmp]){
break;
}
count++;
tmp++;
}
return count;
}
}
return 0;
}
}
使奇数处于偶数的前面
题目描述:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
解题思路:
参考插入排序的解法
(1)从第2个奇数开始,参考插入排序的方法,将该奇数插入到该奇数之前的第一个偶数位置处。
(2)插入过程,参考插入排序的代码。
public class Solution {
public void reOrderArray(int [] array) {
int len=array.length;
int i=0;
while(i<len){
if(array[i]%2==0){
i++;
}else{
//如果是奇数,array[j]为奇数
if(i==0){
i++;
continue;
}
//从第二个数开始,将当前奇数插入到最前一个偶数位置
//这段代码参考插入排序的代码
int j=i;
int tmp=array[i];
while(j-1>=0 && array[j-1]%2==0){
array[j]=array[j-1];
j--;
}
//此时,j指向最前的一个偶数位置。当前奇数插入到该位置
array[j]=tmp;
//继续遍历
i++;
}
}
}
}
顺时针打印矩阵
题目描述:
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下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.
解题思路:
设置4个边界,up、down、left、right,分别完成up边界向右、right边界向下、down边界向左,left边界向上的遍历,每完成一个边界的遍历,对应的边界去除,同时需要满足left<=right;up<=down;程序以这种逐步缩小边界的方式完成顺时针的打印。
直接欣赏一段代码
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> res=new ArrayList<Integer>();
//特判,行、列、本身为空,均返回空值。
if(matrix.length==0 || matrix[0].length==0 || matrix==null){
return res;
}
//定义四个边界left、right、up、down
int left=0;
int right=matrix[0].length-1;
int up=0;
int down=matrix.length-1;
while(true){
//最上边一行,向右扫描,col++;
for(int col=left;col<=right;col++){
res.add(matrix[up][col]);
}
//除去最上面一行,up--;
up++;
//不能超出边界
if(up>down){
break;
}
//向下扫描最右边的一列,row++
for(int row=up;row<=down;row++){
res.add(matrix[row][right]);
}
right--;
if(right<left){
break;
}
//向左扫描最后一行
for(int col=right;col>=left;col--){
res.add(matrix[down][col]);
}
down--;
if(down<up){
break;
}
//向上扫描最左边的一列
for(int row=down;row>=up;row--){
res.add(matrix[row][left]);
}
left++;
if(left>right){
break;
}
}
return res;
}
}
数组中出现超过一半的元素
题目描述:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。
解题思路:
候选法则,核心原理:相同元素则+1,不同则-1,如果超过半数,那最后的计数器必然>1,
(1)初始化一个候选人,并给它一张票
(2)统计这个候选人是否重复出现在数组中,每出现一次,count+1;没有出现,表明票给了其他人,则count-1;
(3)当票数再次变为0时,就选举当前元素。
(4)检查最后的候选人出现的次数,如果超过半数,就输出该候选人,否则输出0;
看一段代码什么都明白,所有的文字解释显得苍白无力.
public class Solution {
public int MoreThanHalfNum_Solution(int [] array) {
//采用候选人法
int con=-1;//候选人
int count=0;//票数
//一张票只能给一个人。
for(int i=0;i<array.length;i++){
if(count==0){//票数为零了,则选举当前元素为候选人,并获得选票
con=array[i];
count++;
}else{
//有后选人获得选票
if(con==array[i]){
count++;
}else{
count--;
}
}
}
//经过一轮头投票之后,能找到候选人
count=0;
//统计该候选人的票数
for(int i=0;i<array.length;i++){
if(con==array[i]){
count++;
}
}
//超过半数输出候选人
return count>array.length/2?con:0;
}
}
top K 问题
题目描述:
从数组中找出最小的k个数
解题分析:
这是典型的top K问题,使用优先队列来模拟大顶堆,大顶堆有如下特点:每次出队的元素都是最大的元素,利用这个特性,我们可以设置一个容量为K的大顶堆,按照数组的顺序入队k个元素,然后后面的元素依次与大顶堆中的元素比较,把小的元素替大顶堆中最大的元素,这样操作下来,就能保证大顶堆中的元素都是相对较小的元素。
注意,在jdk1.8版本中,PriorityQueue默认是小顶对,可以在构造器传入一个容量和比较器,这个比较器可以使用lamaba表达式来实现,(o1,o2) ->o2-o1为大顶堆。
import java.util.*;
public class Solution {
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> result = new ArrayList<Integer>();
int length = input.length;
if(k > length || k == 0){
return result;
}
//大顶堆:每次出出队列都是最大元素,(o1,o2)->(o2-o1);
PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k,(o1,o2)->o2-o1);
for (int i = 0; i < length; i++) {
if (maxHeap.size() != k) {
maxHeap.offer(input[i]);
}else{
if(maxHeap.peek() > input[i]){
maxHeap.poll();
maxHeap.offer(input[i]);
}
}
}
for (Integer integer : maxHeap) {
result.add(integer);
}
return result;
}
}
连续子数组最大和
问题描述:
给定一个数组,求出一个连续子数组,使得它的和最大。
解题思路:
方法1:
动态规划,设dp[i]表示以i为尾元素的最大和,则动态规划的步骤为:
(1)确定dp[i]表示的意义
(2)确定base_case:dp[0]=array[0];
(3)确定状态:最大和
(4)枚举选择,选择最优的一种:选择1为:dp[i-1]为正数,即dp[i-1]+array[i];选择2为:dp[i-1]为负数,即dp[i]=array[i];最后求最大的max(dp[i-1]+array[i],array[i]);
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int[] dp=new int[array.length];
//base_case;
dp[0]=array[0];
int res=array[0];
for(int i=1;i<array.length;i++){
//做出选择
//选择1:array[i]是正数,添加array[i],则,dp[i]=dp[i-1]+array[i];
//选择2:array[i]是负数;
dp[i]=Math.max(dp[i-1]+array[i],array[i]);
res=Math.max(res,dp[i]);
}
return res;
}
}
方法2:
设置一个贡献值,每遍历一个元素,先试探的加上array[i], 如果和为负数,显然,以i结尾的元素对整个结果不作贡献,那设置这个元素的贡献值为0,否则有贡献,加上贡献值。
最后判断整体的贡献值是否为0,如果是,则整个数组中最大的元素为最大子数组的和;
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int res=array[0];
int con=0;//初始化贡献值
for(int i=0;i<array.length;i++){
if(con+array[i]<0){
//负数贡献值为零,不计入
con=0;
}else{
con+=array[i];
res=Math.max(con,res);
}
}
if(con!=0){
//贡献不为0;
return res;
}
//贡献为0
res=array[0];
for(int i=0;i<array.length;i++){
res=Math.max(array[i],res);
}
return res;
}
}
把数组中的数字组合成最小的数
题目描述:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。
解题思路:
本质上是一个排序的过程,只是排序的规则需要改变,例如,对于{a,b}而言,按照题目要求,有两种情况:
(1)"ab">"ba" ,则排序顺序为:b a
(2)"ab"<"ba",则排序顺序为:a b
(3) "ab"=="ba",则排序的顺序任意。
在java中,java.util.Collections工具包有个sort(List,Comparator)方法,可以对list按照传入的Comparator进行比较,我们在这个Comparator中进行上述逻辑的判断;
Comparator的用法:
其内部有个int compare(O1,O2)方法,返回值决定O1和O2如何排序,如果返回<0的数,表明为false,不需要调整原来的顺序,若返回>0的数,需要调整原来的顺序即O2 O1;
直接代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Solution {
public String PrintMinNumber(int [] numbers) {
StringBuilder sb=new StringBuilder();
ArrayList<Integer> list=new ArrayList<Integer>();
for(int i=0;i<numbers.length;i++){
list.add(numbers[i]);
}
Collections.sort(list,new Comparator<Integer>(){
@Override
public int compare(Integer o1,Integer o2){
String str1=o1+""+o2;
String str2=o2+""+o1;
//a b
//ab>ba 则应该b a
//ab<ba a b
// ab==ba 0
int res=str1.compareTo(str2);//str1-str2
if(res>0){
return 1;//需要交换a 和 b
}else if(res<0){
return -1;
}else{
return 0;
}
}
});
for(Integer e:list){
sb.append(e);
}
return sb.toString();
}
}
数组中逆序对问题
题目描述
数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
解题思路:
根据归并排序的改进来求解,在归并排序的过程中,两个子数组[a, b,c] [d , e,f],如果a>d,则需要先将d放入缓存数组,同时(a,d)是一个逆序对,且b和c也能形成逆序对。
直接看代码中解释更容易理解
public class Solution {
int res=0;
public int InversePairs(int [] array) {
if(array.length<2){
return 0;
}
int[] tmp=new int[array.length];
mergeSort(array,0,array.length-1,tmp);
return res;
}
//递归划分,到l==r为止(只有一个元素)
private void mergeSort(int[] array,int l,int r,int[] tmp){
if(l>=r){
return ;
}
//折半划分,先求出mid
int mid=(l+r)/2;
mergeSort(array,l,mid,tmp);
mergeSort(array,mid+1,r,tmp);
merge(array,l,mid,r,tmp);
}
private void merge(int[] array,int l,int mid,int r,int[] tmp){
//合并的过程
//[l,mid]和[mid+1,r]
int i=l;
int j=mid+1;
int k=0;
while(i<=mid && j<=r){
if(array[i]>array[j]){
tmp[k++]=array[j++];
//核心部分,思考关键:
//array[i]>array[j]说明了这两个元素是逆序的关系,并且
//[l,mid]和[mid+1,r]已经排好序,即array[mid]>array[l],故array[1...mid]均符合逆序的关系
//数量为mid-i+1
res+=(mid-i+1);
res=res%1000000007;
}else{
tmp[k++]=array[i++];
}
}
//两部分必然有一部分先遍历完成,将剩余的部分复制tmp
while(i<=mid){
tmp[k++]=array[i++];
}
//右半部分没复制完成
while(j<=r){
tmp[k++]=array[j++];
}
//注意不是从0开始,而是从当前处理的子数组的l开始可能是l或者mid+1;
k=0;
int tmpLeft=l;
while(tmpLeft<=r){
array[tmpLeft++]=tmp[k++];
}
}
}
和为Sum的连续正整数序列
题目描述:
找出所有连续的和为Sum的正整数序列,输出结果按从小到大排列
解题思路:
双指针,由于是连续的正整数序列,所以为等差数列,差值为1,故curSum=(\(a_0\)+\(a_n\))*n/2;通过比较curSum和Sum的大小来移动窗口。分三种情况:
(1)但curSum==sum,找到,将窗口内的所有数据加入结果集合,同时窗口左移动。
(2)surSum< sum,窗口右边扩大,增加了一个元素,
(3)curSum>sum,左边窗口右移动,减少了一个元素,
import java.util.ArrayList;
public class Solution {
public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
//利用双指针,连续的正整数[1,2,...n]
//初始化窗口[1,2],至少含有两个元素。
ArrayList<ArrayList<Integer>> res=new ArrayList<ArrayList<Integer>>();
int left=1;
int right=2;
while(left<right){
int curSum=(left+right)*(right-left+1)/2;//标准的求和公式
ArrayList<Integer> list=new ArrayList<Integer>();
if(curSum==sum){
//将窗口内的数据全部加入进来
for(int i=left;i<=right;i++){
list.add(i);
}
res.add(list);
left++;
}else if(curSum<sum){
//右边窗口移动
right++;
}else{
left++;
}
}
return res;
}
}
数学相关问题
不用加法做加法
题目描述:
不使用+ - * /运算符求解两个整数的加法
解题思路:
将两个整数转化位二进制num1和num2;
(1)不考虑进位情况,求两个二进制位相加结果,即int sum=num1^num2;
(2)求解两数相加的进位,两数相与在左移一位,即int carry=(num1&num2)<<1;
(2)上述两个步骤的结果再次循环计算,直到carry进位为0为止。
public class Solution {
public int Add(int num1,int num2) {
while(num2!=0){
int sum=num1^num2;//不考虑进位,两数相加等同于 ^
int carry=(num1&num2)<<1;//计算进位,按位与 在左移1位
num1=sum;
num2=carry;
}
return num1;
}
}
扩展:
不使用+ - * /做1+2+3+4+...+n
原理一样,直接看代码
public class Solution {
public int Sum_Solution(int n) {
int sum=0;
for(int i=1;i<=n;i++){
sum=add(sum,i);
}
return sum;
}
private int add(int num1,int num2){
while(num2!=0){
int sum=num1^num2;
int carry=(num1&num2)<<1;
num1=sum;
num2=carry;
}
return num1;
}
}
快速幂
题目描述:
给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。保证base和exponent不同时为0
解题步骤:
(1)考虑exponent为正数的情况。从右往左,对exponent的二进制进行逐一求解,如果该位为1,则res=resbase;否则该位为0,不需要计算,直接进入下一位
(2)每计算一位,就抛弃该位,也就是>>>1的操作
(3)base=base*base;
(4)如果exponent为负数,则最后的结果res=1/res;
public class Solution {
public double Power(double base, int exponent) {
boolean flag=exponent>0;
double res=1;
exponent=Math.abs(exponent);
while(exponent!=0){
if((exponent & 0x1)!=0){
res*=base;
}
base*=base;
exponent = exponent >>> 1;//抛弃最低位,下一轮循环可以直接和1做&运算即可。
}
if(flag){
return res;
}
return 1/res;
}
}
整数二进制中的1的个数
解题思路:
从最高位开始,分别与flag=1进行&运算,如果为1 ,表明当前位为1,计数器加一,进入下一个循环之前,可以将falg=flag<<1,这等同于将1移到了次高位。再次循环上面的操作。
public class Solution {
public int NumberOf1(int n) {
int count=0;
int flag=1;
while(flag!=0){
if((n & flag)!=0){
count++;
}
flag=flag << 1;
}
return count;
}
}
丑数的个数
题目描述:
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
解题思路:
根据定义,丑数一定是:前一个丑数的2倍或3倍或者5倍。按照从小到达的顺寻来排列,当前丑数根据上面3中情况选一种,如此迭代更新,index-1次,便可以求得
public class Solution {
public int GetUglyNumber_Solution(int index) {
if(index<=6){
return index;
}
int[] res=new int[index];
int t1=0;
int t2=0;
int t3=0;
res[0]=1;//第一个丑数
for(int i=1;i<index;i++){
res[i]=Math.min(res[t1]*2,Math.min(res[t2]*3,res[t3]*5));
if(res[i]==res[t1]*2){
t1++;
}
if(res[i]==res[t2]*3){
t2++;
}
if(res[i]==res[t3]*5){
t3++;
}
}
return res[index-1];
}
}
二分查找
旋转数组找最小
题目描述:
输入一个旋转数组,找出这个数组中的最小值,要求时间复杂度为(logn)
题目分析:
- 旋转数组:指的是将一个有序的数组的前一部分移动到后一部分,如[1,2,3,4,5]的一个旋转数组为:[2,4,5,1,2]
- 时间复杂度为logn:立刻想到二分法(折半查找)
- 二分查找一般需要个固定的比较值,然而这道题却没有,怎么办?根据题意具体分析有以下几种情况:
(1)[4,5,6,1,2,3],arr[mid]>arr[first],故min在区间[mid+1,last]
(2)[4,5,1,2,3] arr[mid]< arr[last],故min在区间[first,mid]
(3)[1,1,1,0,1],则,first,向左移动一位;
代码
import java.util.ArrayList;
public class Solution {
public int minNumberInRotateArray(int [] array) {
if(array.length==0){
return 0;
}
int i=0;//首元素
int j=array.length-1;
while(i<j){
if(array[i]<array[j]){//没有进行旋转的一种特殊情况
return array[i];
}
int mid=(i+j)/2;
if(array[i]<array[mid]){
//要找的最小值必然在右半部分
i=mid+1;
}else if(array[j]>array[mid]){
j=mid;
}else{
i++;
}
}
//特例:只有一个元素
return array[i];
}
}
[1,n]整数数组中找1出现的总个数
题目描述:
求出113的整数中1出现的次数,并算出1001300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
解题思路:
从最低位向最高位依次取出当前值cur,并计算cur的高位high和低位low;
当cur==0时,出现的1的个数=high*i;
当cur==1时,出现1的个数=high*i+(low+1);
其他情况,出现1的个数=(high+1)*i;
最后计算从最低位到最高位的总和
public class Solution {
public int NumberOf1Between1AndN_Solution(int n) {
int count=0;
for(int i=1;i<=n;i*=10){
int high=n/(i*10);
int low=n%i;
int curVal=n/i%10;
if(curVal==0){
count+=high*i;
}else if(curVal==1){
count+=high*i+(low+1);
}else{
count+=(high+1)*i;
}
}
return count;
}
}
在二维数组中查找值
题目描述:
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
题目分析:
从左下角元素(curVal)开始寻找,这个元素是这行中最大的数,这列中最小的数,如果target>curVal,只能从下一列中开始寻找,即col++,如果target< curVal,只能从上一行中寻找,即row--;注意 col<=cols-1;row>=0;
public class Solution {
public boolean Find(int target, int [][] array) {
//最后一列的中间值
int rows=array.length;//二维数组的行数
int cols=array[0].length;//二维数组的列数
if(rows==0){
return false;
}
if(cols==0){
return false;
}
//左下角
int row=rows-1;
int col=0;
while(row>=0 && col<=cols-1){
if(array[row][col]<target){
col++;
}else if(array[row][col]>target){
row--;
}else{
return true;
}
}
return false;
}
}
递归问题
机器人运动范围
题目描述:
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
解题分析:
(1)利用递归求解,递归的结束条件:当前格子的坐标数位之和>设定的阈值,或者不在[0,rows-1] [0,cols-1]范围内,为了避免重复搜索,需要设置一个flag[curRow][curRow]用于记录当前格子被浏览过。
(2)check()函数的实现。用n%10取最低位,n/10取除最低位之外的整数;依次循环直到n=0为止,即可求出每一位
public class Solution {
public int movingCount(int threshold, int rows, int cols){
int[][] flag=new int[rows][cols];
return movingCountHelper(threshold,rows,cols,0,0,flag);
}
private int movingCountHelper(int threshold,int rows,int cols,int curRow,int curCol,int[][] flag){
//特判,当前格子不在[0,rows]和[0,cols]
if(curRow<0 || curRow>rows-1 || curCol<0 || curCol>cols-1 || flag[curRow][curCol]==1 || !check(curRow,curCol,threshold)){
return 0;
}
flag[curRow][curCol]=1;//设置当前格子已经走过;
//递归查找,向左、右、上、下分别递归寻找。
return 1+movingCountHelper(threshold,rows,cols,curRow,curCol+1,flag)
+movingCountHelper(threshold,rows,cols,curRow,curCol-1,flag)
+movingCountHelper(threshold,rows,cols,curRow+1,curCol,flag)
+movingCountHelper(threshold,rows,cols,curRow-1,curCol,flag);
}
//检查当前坐标
private boolean check(int row,int col,int threshold){
int res=0;
while(row!=0){
res+=row%10;//取最低位;
row=row/10;//取除去最低位的数;
}
while(col!=0){
res+=col%10;
col=col/10;
}
if(res>threshold){
return false;
}
return true;
}
}
从矩阵种匹配字符串
题目描述
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。
题目分析
和上一题几乎是一模一样的思想,递归+回溯,递归的本质就是只负责自己的活,下一步的逻辑和当前逻辑一样(调用自己)
(1)递归函数的功能就是判断当前坐标位置的值和str中对应的值是否相同,如果相同,则可以选择上、下、左、右任意一个方向进行递归的匹配,递归体现在:str中的字符向下一位;
(2)需要设置一个变量visited[][]用于记录当前的格子是否被访问过。当前匹配成功,需要将其设置为1;
(2)如果当前格子中的字符和str[i]的字符匹配不成功,则需要回溯到原来的状态(visted[][]==flase),设置当前的格子未被浏览,即是回溯的操作;
public class Solution {
public boolean hasPath(char[] matrix, int rows, int cols, char[] str){
if(rows*cols<str.length){
return false;
}
int[][] flag=new int[rows][cols];
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
if( pathHelper(matrix,rows,cols,i,j,str,0,flag)){
return true;
}
}
}
return false;
}
private boolean pathHelper(char[] matrix,int rows,int cols,int curRow,int curCol,char[] str,int curChar,int[][] flag){
//将二维数组转化为一维数组
int index=curRow * cols+curCol;
if(curRow<0 || curRow>=rows || curCol<0 || curCol>=cols || flag[curRow][curCol]==1 || matrix[index]!=str[curChar]){
return false;
}
//字符串全部匹配完成
if(curChar==str.length-1){
return true;
}
//否则,该路径可以
flag[curRow][curCol]=1;//记录这条路已经走了
if(pathHelper(matrix,rows,cols,curRow-1,curCol,str,curChar+1,flag)
|| pathHelper(matrix,rows,cols,curRow+1,curCol,str,curChar+1,flag)
|| pathHelper(matrix,rows,cols,curRow,curCol+1,str,curChar+1,flag)
|| pathHelper(matrix,rows,cols,curRow,curCol-1,str,curChar+1,flag)
){
return true;
}
//走到这一步,必然匹配失败,进行回溯
flag[curRow][curCol]=0;//回溯
return false;
}
}
矩形覆盖问题
题目描述:
我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法
解题思路:
public class Solution {
public int RectCover(int target) {
if (target < 1) {
return 0;
} else if (target == 1 || target == 2) {
return target;
} else {
//target-1 表示上一个矩形
return RectCover(target-1) + RectCover(target-2);
}
}
}
跳阶梯问题I
问题描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解题步骤:
暴力枚举:设dp[i]表示跳到第i阶的跳数,则dp[n]为所求解,
初始化条件:dp[0]=dp[1]=1;
核心思想,如果从上一级跳1步到第i级,则上一级为 i-1;
如果上一级跳2步到i,则上一级为i-2;
public class Solution {
public int JumpFloorII(int target) {
//dp[i],表示跳上i级台阶的跳法
//先计算跳上i级台阶的跳数,再计算从i级到target的跳数
int[] dp=new int[target+1];
dp[0]=1;//跳上1级台阶的跳法
dp[1]=1;
for(int i=2;i<=target;i++){
//枚举跳上i的跳法
for(int j=0;j<i;j++){
dp[i]+=dp[j];
}
}
return dp[target];
}
}
跳阶梯问题II
题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
解题思路:
先来分析问题:
设:dp[i]表示跳到i级台阶的总跳法,由于只能跳1步或者2步,那么从上一步只可能为:i-1或者i-2,即dp[i]=dp[i-1]+dp[i-2];
很显然,这是一个动态规划的问题:
base_case:dp[0]=1,dp[1]=1;
public class Solution {
public int JumpFloor(int target) {
int[] dp=new int[target+1];
dp[0]=1;
dp[1]=1;
for(int i=2;i<=target;i++){
dp[i]=dp[i-1]+dp[i-2];
}
//dp[target];
return dp[target];
}
}
跳台接问题III
问题描述:
给定一个非负整数数组,数组元素表示你在该位置能可以跳跃的最大长度,判断你是否能跳到最后的位置
例如:[2,3,1,1,4],可以从初始位置0先跳1步到达1位置,然后再跳3步及到达最后位置,故返回true
解题步骤:
可以使用贪心算法,这道题目可以换个思路:从位置i最远可以跳多远fast,如果这个fast>n-1,则必然可以跳到最后的位置
public class Solution {
public int JumpFloor(int[] nums) {
int fastest=0;
for(int i=0;i<nums.length-1;i++){
fastest=Math.max(fastest,i+nums[i]);
if(fastest<=i){
return false;
}
}
//长度为n的数组,最多需要跳n-1步;
return fastest>=n-1;
}
}
跳台阶问题IIII
在上述“跳台阶问题III”的基础上,求最少的跳的步数到达最后一个位置
解题思路:
使用谈心算法,每一步跳到最大位置,然后再从这个位置又选择最大的位置进行跳跃
public class Solution {
public int JumpFloor(int[] nums) {
int fastest=0;
int end=0//记录每一跳的结束位置
int jump=0;
for(int i=0;i<nums.length-1;i++){
fastest=Math.max(fastest,i+nums);
//跳到最远位子再计数
if(i==end){
jump++;
end=fastest;//将当前fastest更新到全局,
}
}
}
}
栈
数据流中的中位数问题
题目描述:
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。
题目的分析:
这道题目要使用到PriorityQueue优先级队列,我们先来介绍优先级队列,它是一种特殊的队列,只不过在出队列的时候,从队首出队的元素有个优先级别(也即总是出队最大值,或总是出队最小值),这个可以设置:
- new PriorityQueue((a,b)->a-b);队列内部是升序排序(默认),即队首的元素永远为当前值的最小值,所以是minHeap;
- new PriorityQueue((a,b)->b-a);队列内部是降序排序,即队首元素为最大值,所以是:maxHeap;
由于不知道数据动态加入,且数据的长度的奇偶性不确定,我们可以使用一个count来奇数,同时为了保证数据能动态均匀地加入到maxHeap和minHeap中,我们约定: 当count为奇数时,数据加入maxHeap,否则加入minHeap,为了保证maxHep和minHeap的队首原属为中位数,我们需要保证 待加入的元素num必须与要符合:maxHeap队首元素<=num<=minHeap队首元素,可以参见下图来辅助理解:
如图所示:待插入元素是5,按照上述约定(插入元素为奇数个,插入maxHeap中),需要保证5插入到maxHeap之后符合:“maxHeap中队首元素<=minHeap队首的元素”,如果满足直接插入,否者,需要作如下调整:先将5插入到minHep中(minHeap内部将会自动降序排列,此时取出的minHeap队首的元素必然符合上述条件),偶数时,同样的道理,具体可以参考代码,更好理解
import java.util.*;
public class Solution {
//创建大顶堆(用于升序排序),lambada表达式实现comparator比较器
Queue<Integer> maxHeap=new PriorityQueue<Integer>((a,b)->b-a);
Queue<Integer> minHeap=new PriorityQueue<Integer>((a,b)->a-b);
//记录数据的总个数,奇偶分别处理
//为了保证数据能均匀分配道两个堆中去,可以约定,count为偶数时候
//先入maxHeap,否则入,minHeap;同时需要保证:maxHeap的最大元素<=minHeap中的最小元素
int count=-1;
public void Insert(Integer num) {
count++;
if(count%2==0){//偶数入maxHeap,入之前先判断
if(!minHeap.isEmpty() && num>minHeap.peek()){
minHeap.add(num);
num=minHeap.poll();
}
maxHeap.add(num);
}else{//奇数
if(!maxHeap.isEmpty() && num<maxHeap.peek()){
//保证,加入到minHeap中的元素num不能小于maxHeap.peek(),否则要交换
maxHeap.add(num);
num=maxHeap.poll();//内部自动升序排序
}
minHeap.add(num);
}
}
public Double GetMedian() {
//取出的同样要分奇、偶性质
if(maxHeap.size()==minHeap.size()){
//取两个优先机队列的顶部元素的平均值
return (maxHeap.peek()+minHeap.peek())/2.0;
}else if(maxHeap.size()>minHeap.size()){
return maxHeap.peek()/1.0;
}else{
return minHeap.peek()/1.0;
}
}
}
判断栈的入栈顺序和出栈顺序是否一致
题目描述:
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
解题步骤:
(1)特判:当popA和pushA数组的长度有一个为空,必然不一致
(2)先将pushA中的第一个元素入栈。
(3)设置两个指针i和j分别指向pushA和popA,判断stack栈顶的元素是否和popA[j]元素相同,如果相同,则出栈,j指向下一位,如果不同,则循环执行(3)直到相同为止,若i的索引超出步骤,则表明他们顺序不会一致,返回false;
直接看代码
import java.util.ArrayList;
import java.util.Stack;
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA) {
if(pushA.length==0 || popA.length==0){
return false;
}
Stack<Integer> stack=new Stack<Integer>();
int i=0;//pushA的指针
int j=0;//popA的指针;
stack.push(pushA[i++]);
while(j<popA.length){
while(stack.peek()!=popA[j]){
//继续入栈直到popA[j]元素相同为止
if(i<pushA.length){
stack.push(pushA[i++]);
}else{
return false;
}
}
//i<pushA.length && peek()==pop[j]
//出栈
stack.pop();
j++;
}
return true;
}
}
包含Min函数的栈结构
题目描述:
定义一个栈结构,该栈中有个能获取当前栈中最小元素的函数min,要求算法的时间复杂度为O(1)
解题思路:
这道题是典型的空间换时间的思想,要定义两个栈,一个栈表示本体stack1,另一个栈stakc2存储当前栈中的最小值,在入栈时,需要判断待入栈的元素和stack2栈顶元素的大小,只有value<=stack2.peek()时,才入stack2,这样就保证stack2的栈顶一定为当前栈的最小元素。出栈时,如果stack2和stack1的栈顶元素相同,则stack2中的元素也必须同时更新出栈。
直接看代码
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);
if(stack2.isEmpty()){
stack2.push(node);
}else if(stack2.peek()>=node){
//stack2中只保存最小值
stack2.push(node);
}
}
public void pop() {
if(stack1.peek()==stack2.peek()){
stack2.pop();
}
stack1.pop();
}
public int top() {
return stack1.peek();
}
public int min() {
return stack2.peek();
}
}
链表相关
删除重复链表
题目描述:
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
解题思路:
(1)新建一个指向头节点pHead的新节点Head
(2)tmp指向链表的实际头节点pHead
(3)同时需要一个pre节点来保存当前节点的前一节点的,初始化时,pre=Head;以便于删除重复节点后能连接后续的节点
分两种情况讨论:
当头节点就有相同的节点的时候,此时,Head.next=tmp.next;
当相同的节点不在头节点的时候,pre=pre.next;(指向当前链表的头节点),tmp=tmp.next;
图解算法
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode deleteDuplication(ListNode pHead){
if(pHead==null || pHead.next==null){
return pHead;
}
ListNode Head=new ListNode(0);
Head.next=pHead;
ListNode pre=Head;
ListNode tmp=Head.next;
while(tmp!=null){
if(tmp.next!=null && tmp.val==tmp.next.val){
while(tmp.next!=null && tmp.val==tmp.next.val){
tmp=tmp.next;
}
pre.next=tmp.next;
tmp=tmp.next;
}else{//头节点不相同的情况
pre=pre.next;
tmp=tmp.next;
}
}
return Head.next;
}
}
链表中找环的入口
题目描述:
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。
解题思路:
(1)使用快慢指针,fast=fast.next.next;slow=slow.next;快指针走过的路程为慢指针的两倍。如果有环,两者必然会相遇。
(2)第一次相遇后,将slow复原到pHead,fast仍然从相遇点出发,当两者以相同的速读再次前进时,下一次相遇点则为环的起点。
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead){
if(pHead==null || pHead.next==null){
return null;
}
ListNode slow=pHead;
ListNode fast=pHead;
while(fast!=null && fast.next!=null){
fast=fast.next.next;
slow=slow.next;
if(slow==fast){
break;
}
}
//第一次相遇,此时让slow回到起点,slow和fast以相同的速度前行,
//他们的再次相遇点位环的起点
slow=pHead;
while(slow!=fast){
fast=fast.next;
slow=slow.next;
}
//相遇,slow为环状的起点
return slow;
}
}
约瑟夫环问题
问题描述:
首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
解题步骤:
(1)用java中的list集合来模拟约瑟夫环问题,模拟环状的关键在于:index%list.size();
(2)初始化index为要删除的节点前一个位置,index+m则表示为当前要删除的节点位置,删除完成节点后,需要将index--;即移动到前一个位置,便于下次循环继续删除。
import java.util.*;
public class Solution {
public int LastRemaining_Solution(int n, int m) {
//特判
if(n==0 || m==0){
return -1;
}
LinkedList<Integer> list=new LinkedList<Integer>();
//创建循环链表
for(int i=0;i<n;i++){
list.add(i);
}
//表示index在头节点之前的位置
int index=-1;
while(list.size()>1){
index=(index+m)%(list.size());
list.remove(index);
index--;//将index回到头节点之前的位置,即上一个节点
}
return list.get(0);
}
}
两条链表中的公共节点
题目描述:
输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
解题思路分析
(1)使用双指针方法,同时遍历两条链表。
(2)如果存在公共节点,那么,两个指针必然会相交。
(3)但是两条链表的长短不一,这种情况下,必然有一个指针会先停下来(如果没有找到公共节点),这个时候,就让这个指针指向另外一条链表的头节点,但另外一个指针也结束后,也让他指向另外一条链表。如此反复进行遍历,如果有节点,他们必将指向同一个节点。
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if(pHead1==null && pHead2==null){
return null;
}
if(pHead1==null || pHead2==null){
return null;
}
ListNode p=pHead1;
ListNode q=pHead2;
while(p!=q){
p=p.next;
q=q.next;
if(p!=q && p==null){
p=pHead2;
}
if(p!=q && q==null){
q=pHead1;
}
}
return p;
}
}
复制复杂的链表问题
问题描述:
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
解题思路:
由于存在Random指针,可以考虑用map将原链表中的每个节点的值保存起来。然后依次按照链表的的节点顺序从map中取出复制的节点,用于构建新的链表。分别用p、q指针指向原链表和目标链表。
直接上代码更加清晰动人
/*
public class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;
RandomListNode(int label) {
this.label = label;
}
}
*/
import java.util.*;
public class Solution {
public RandomListNode Clone(RandomListNode pHead){
if(pHead==null){
return null;
}
RandomListNode p = pHead;
//新建节点
RandomListNode target = new RandomListNode(p.label);
RandomListNode q=target;
HashMap<RandomListNode,RandomListNode> map=new HashMap<RandomListNode,RandomListNode>();
//创建两条链表之间的映射关系
while(pHead!=null){
map.put(pHead,new RandomListNode(pHead.label));
pHead=pHead.next;
}
//开始复制
while(q!=null){
q.next=map.get(p.next);//p点的下一个节点
q.random=map.get(p.random);//p的random节点
p=p.next;
q=q.next;
}
return target;
}
}
合并两条链表
题目描述:
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
思路分析:
(1)设置两个指针p和q分别指向两条链表,新建一条target链表用作最后的结果返回,同时给target新建一个辅助指针t:t=target
(2)比较p和q节点的值,分情况讨论,存在三种情况:
- p.val< q.val,则t的下个节点接p
- p.val==q.val,则t需要分别接p和q
- p.val>q.val,则他需要接q;
(3)最后,可能存在两条链表不一样长的情况,同时需要考虑两种情况,把长的剩余的链表直接到t的后面
(4)最后注意是返回target.next
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
if(list1==null && list2==null){
return null;
}
if(list1==null){
return list2;
}
if(list2==null){
return list1;
}
ListNode p=list1;
ListNode q=list2;
ListNode target=new ListNode(-1);
ListNode t=target;
while(p!=null && q!=null){
if(p.val<q.val){
t.next=p;
p=p.next;
t=t.next;
}else if(p.val==q.val){
t.next=p;
p=p.next;
t=t.next;
t.next=q;
q=q.next;
t=t.next;
}else{
t.next=q;
q=q.next;
t=t.next;
}
}
//至少有一条链表先完成
if(p==null){
t.next=q;
t=t.next;
}else if(q==null){
t.next=p;
t=t.next;
}else{
return target.next;
}
return target.next;
}
}
链表的反转
题目描述:
给定一个链表,求出该链表的反转链表
解题思路1:
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head==null){
return null;
}
ListNode pre=null;
ListNode cur=head;
ListNode next=null;
while(cur!=null){
next=cur.next;
//连接逆转链表
cur.next=pre;
//三个指针向后移动
pre=cur;
cur=next;
}
return pre;
}
}
解题思路2:
这种方式需要耗费额外的内存(需要复制每一个节点)
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head==null){
return null;
}
ListNode p=head;
ListNode target=new ListNode(-1);
ListNode t=target;
while(p!=null){
ListNode q=new ListNode(p.val);//需要复制节点
q.next=t.next;
t.next=q;
// 拼接完成
p=p.next;
}
return target.next;
}
}
输出链表倒数第K个节点
题目描述:
输入一个链表,输出该链表中倒数第k个结点。
解题思路:
(1)注意特判情况,当链表的长度小于输入参数K时,无效
(2)设置p和q两个指针,让p先走k步,然后让p和q同步走,当q为null时,此时p所指向的节点即为倒数第k个节点
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
if(count(head)<k){
return null;
}
ListNode p=head;
ListNode q=head;
for(int i=0;i<k;i++){
q=q.next;
}
while(q!=null){
q=q.next;
p=p.next;
}
return p;
}
private int count(ListNode head){
if(head==null){
return 0;
}
ListNode tmp=head;
int count=0;
while(tmp!=null){
count++;
tmp=tmp.next;
}
return count;
}
}
从尾到头打印链表
解题思路:
(1)先反转链表,然后再遍历即可
(2)使用栈先存储链表,然后从栈中取出数据并打印;
这里以第(2)种方案为案例进行代码演示:
/**
* public class ListNode {
* int val;
* ListNode next = null;
*
* ListNode(int val) {
* this.val = val;
* }
* }
*
*/
import java.util.*;
public class Solution {
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
ArrayList<Integer> res=new ArrayList<Integer>();
if(listNode==null){
return res;
}
Stack<Integer> stack=new Stack<Integer>();
ListNode tmp=listNode;
while(tmp!=null){
stack.add(tmp.val);
tmp=tmp.next;
}
while(!stack.isEmpty()){
res.add(stack.pop());
}
return res;
}
}
树
树的前、中、后序遍历的非递归方法
前序遍历
解题思路:
前序遍历的顺序为:根节点,左子树,右子树,但是用非递归的方法时候,需要用到栈结构,为了保证前面的所提的顺序,因此应该先入右节点,再入左节点,这样的话出栈才能保证先左、后右边!
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution{
public void preOrder(TreeNode root){
Stack<TreeNode> stack=new Stack<>();
//特判
if(root==null){
return;
}
stack.push(root);
while(!stack.isEmpty()){
TreeNode head=stack.pop();
//访问根节点的操作。。。
if(head.right!=null){
stack.push(head.right);
}
if(head.left!=null){
stack.push(head.left);
}
}
}
}
中序遍历
解题思路:
根据中序遍历的特点,对于任意节点,先访问其左节点(如果有的话),在访问该节点,最后在访问右节点(如果有的话),因此需要使用到栈结构:
核心思想就是,从根节点开始,将所有的左节点入栈,然后依次出栈,并访问该元素,如过当前元素还有右节点,需要继续使用上述逻辑,入栈当前节点的左节点到底,然后出栈并访问。
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution(){
public void inorder(TreeNode root){
Stack<TreeNode> stack=new Stack
if(root==null){
return ;
}
TreeNode tmp=root;
while(tmp!=null || stack.isEmpty()){
//左节点一直入栈至空为止
while(tmp!=null){
stack.push(tmp);
tmp=tmp.left;
}
//这个已经到最左子树的叶子节点了
if(!stack.isEmpty()){
tmp=stack.pop();
//访问当前节点
System.out.print(tmp.val);
//tmp指向右节点,即下一轮,右节点入栈;
tmp=tmp.right;
}
}
}
}
后序遍历
解题思路:
在中序遍历的基础上,由于必须要保证先左、右、根节点,因此如果栈顶元素无右子树或者右子树已经被访问,则可以访问当前节点,否则,继续遍历右子数
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution{
public void postOrder(TreeNode root){
Stack<TreeNode> stack=new Stack<>();
TreeNode tmp=root;
TreeNode lastVisited=root;//已经看遍历过的节点
while(tmp!=null || stack.isEmpty()){
//遍历左节点
while(tmp!=null){
stack.push(tmp);
tmp=tmp.left;
}
//先查看栈顶元素
tmp=stack.peek();
//若tmp无右节点或者右节点已经被访问过,则访问当前tmp节点
if(tmp.right==null || lastVisited==tmp.right){
//访问当前节点
System.out.println(tmp);
//设置当前节点访问过
lastVisited=tmp;
tmp=null;
}else{
tmp=tmp.right;
}
}
}
}
//双栈法
public void posOrderUnRecur1(Node head){
//非递归,后序遍历,双栈法
if(head == null) return;
Stack<Ndoe> s1 = new Stack<>();
Stack<Ndoe> s2 = new Stack<>();
s1.push(head);
//子树根先入s1,再出栈入s2,同时它的先左后右子树入s1
//左右子树出s1入s2后,变成右在下,左在上,出s2后顺序变成 左-右-中
while(!s1.isEmpty()){
head = s1.pop();
s2.push(head);
if(head.left != null){
s1.push(head.right);
}
if(head.right!=null){
s1.push(head.right);
}
}
//s2出栈即访问
while(!s2.isEmpty()){
System.out.print(s2.pop().val+" ");
}
}
构建二叉树并前、中、后序遍历
import java.util.Scanner;
import java.util.Stack;
public class Main{
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
//构建二叉树
int count =sc.nextInt();
int rootVal=sc.nextInt();
TreeNode root=null;
if(count==1){
root=new TreeNode(rootVal);
print(root);
return;
}
root= createTree(sc);
//遍历二叉树(前、中、后)
print(root);
}
//定义树节点
private static class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val){
this.val=val;
}
}
//根据输入流来构建二叉树
//递归建立二叉树
private static TreeNode createTree(Scanner sc){
int rootVal=sc.nextInt();
int leftVal=sc.nextInt();
int rightVal=sc.nextInt();
TreeNode root=new TreeNode(rootVal);
if(leftVal!=0){
root.left=createTree(sc);
}
if(rightVal!=0){
root.right=createTree(sc);
}
return root;
}
//前序遍历(非递归法)
private static void preOrder(TreeNode root){
if(root==null){
return;
}
Stack<TreeNode> stack=new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode head=stack.pop();
System.out.print(head.val+" ");
if(head.right!=null){
stack.push(head.right);
}
if(head.left!=null){
stack.push(head.left);
}
}
}
//中序遍历(非递归法)
private static void inOrder(TreeNode root){
if(root==null){
return;
}
Stack<TreeNode> stack=new Stack<>();
TreeNode tmp=root;//辅助指针
while(!stack.isEmpty() || tmp!=null){
//遍历左子树
while(tmp!=null){
stack.push(tmp);
tmp=tmp.left;
}
if(!stack.isEmpty()){
//判断栈顶元素是否有右子树
tmp=stack.pop();
//先遍历当前节点
System.out.print(tmp.val+" ");
tmp=tmp.right;
}
}
}
//后序遍历(非递归法)
private static void postOrder(TreeNode root){
if(root==null){
return ;
}
TreeNode tmp=root;//辅助指针;
TreeNode lastVisited=root;//记录节点被访问过
Stack<TreeNode> stack=new Stack<>();
while(!stack.isEmpty() || tmp!=null){
while(tmp!=null){
stack.push(tmp);
tmp=tmp.left;
}
tmp=stack.peek();
//当没有右节点或者右节点已经被访问过,则可以访问当前节点
if(tmp.right==null || lastVisited==tmp.right){
System.out.print(tmp.val+" ");
stack.pop();
lastVisited=tmp;//设置已经访问过
tmp=null;
}else{
tmp=tmp.right;//否则,继续入栈
}
}
}
private static void print(TreeNode root){
preOrder(root);
System.out.println();
inOrder(root);
System.out.println();
postOrder(root);
}
}
重构二叉树(中等难度)
题目描述:
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解题思路分析:
前序遍历:从根节点开始,先访问根节点,然后左子树,再右子树;中序遍历:先左子树,再根节点,最后右子树;
算法思想:
(1)pre数组中的第一个节点则一定是root,并且root一定再in的中间位置,故在in中找到root所在的位置i,则in[0,i)为root的左子树,左子树的节点树为i,in[i+1,in.length)为root的右子树,
(2)pre中的左子树为:pre[ 1,i+1),长度为i;右子树的范围为[ i+1,pre.length]
(3)in中的左子树为:in[ 0,i);右子树为:in[ i+1,in.length]
(4)递归遍历,直到pre和in数组为空才结束。
代码实现
import java.util.Arrays;
public class Solution {
public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
//特判,空树
if(pre.length==0 || in.length==0){
return null;
}
TreeNode root=new TreeNode(pre[0]);//创建一个根节点
//在in中找到根节点
for(int i=0;i<in.length;i++){
if(in[i]==pre[0]){
//左子树,注意Arrys.copyOfRange为左闭又开空间
root.left=reConstructBinaryTree(Arrays.copyOfRange(pre,1,1+i),Arrays.copyOfRange(in,0,i));
root.right=reConstructBinaryTree(Arrays.copyOfRange(pre,1+i,pre.length),Arrays.copyOfRange(in,i+1,in.length));
break;//下次递归进入for循环,因此找完左右子树后就要结束整个循环。
}
}
return root;
}
}
树的镜像(简单)
题目
操作给定的二叉树,将其变换为源二叉树的镜像。
思路分析:
使用递归的思想,明确递归的结束条件,和调用自己的条件;
(1)当root为空时,直接结束当前函数
(2)当前root的左节点和右节点交换
(3)从左子树开始使用同样的逻辑开始递归;从右子树开始使用同样的逻辑递归.
public class Solution {
public void Mirror(TreeNode root) {
if(root==null){
return ;
}
//交换左、右子节点
TreeNode tmp=root.left;
root.left=root.right;
root.right=tmp;
//递归左子树
if(root.left!=null){
Mirror(root.left);
}
//递归右子树
if(root.right!=null){
Mirror(root.right);
}
}
}
求树的深度(简单)
原理
利用递归的方式,使用递归时,首先得出递归的条件,然后进入递归调用;
import java.lang.Math.*;
public class Solution {
public int TreeDepth(TreeNode root) {
if(root==null){
return 0;
}
//左子树和右子树以同样的逻辑判断,没向下走一层,结果应该加一,每一次递归,针对的树的根节点
return (Math.max(TreeDepth(root.left),TreeDepth(root.right))+1);
}
}
树的平衡性检测(简单)
题目描述
给定一颗树,判断其是否为平衡二叉树
思路分析
平衡二叉树的定义:左右子树的深度差不能超过1,当root为null时,该树必为平衡二叉树,我们需要做的是:检测该树的所有左右子树,判断其是否符合平衡二叉树的定义。
编程的关键
(1)getDepth(),给定根节点,求该节点的深度;
(2)判断一颗树,是否为符合平衡二叉树的定义;
(3)按照相同的判断逻辑,递归调用,判断这颗树的左子树、右子树是否均为平衡二叉树;
public class Solution {
public boolean IsBalanced_Solution(TreeNode root) {
//特判
if(root==null){
return true;
}
//判断当前树是否为平衡二叉树
if(Math.abs(getDepth(root.left)-getDepth(root.right))>1){
return false;
}
//递归判断当前节点的左、右子树是否都是平衡树
return IsBalanced_Solution(root.left) && IsBalanced_Solution(root.right);
}
//获取一颗树的深度
public int getDepth(TreeNode root){
if(root==null){
return 0;
}
return Math.max(getDepth(root.left),getDepth(root.right))+1;
}
}
树的层序遍历(中等)
题目描述
层序遍历,从上到下,从左到右遍历二叉树,每一层的节点存储在一个list中,最后将所有的list存储到结果集中。
思路分析
需要用到两个队列作为缓冲,第一个队列用来缓存当前层的节点,第二个队列用来缓存当前层的下一个节点;每次循环,应该保证队列1中的所有元素都入了队列2,然后依次取出队列二中的元素放入list中;
使用到的java库函数:
(1)队列,其实现的是java.util.Queue接口和java.util.List接口,所以应该导入这两个包,故:Queue q=new LinkedList();
(2)队列常见的方法:q.offer()入队,q.poll()出队,q.peek()查看队首元素,但是不出队列。
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution {
ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
//用于存储最后的结果集合
ArrayList<ArrayList<Integer>> listOuter=new ArrayList<ArrayList<Integer>>();
//队列1缓存每一层的值
Queue<TreeNode> queue=new LinkedList<TreeNode>();
//队列2缓存当前层的下一层的值
Queue<TreeNode> queue2=new LinkedList<TreeNode>();
if(pRoot==null){
return listOuter;
}
//1.根节点先入队列1
queue.offer(pRoot);
while(!queue.isEmpty()){
//一次性将队列1中的所有根节点都取出并放进队列2中
while(!queue.isEmpty()){
TreeNode curNode=queue.poll();
queue2.offer(curNode);
}
ArrayList<Integer> listInner=new ArrayList<Integer>();
//当前层的左右节点即为下一层的根节点
//
while(!queue2.isEmpty()){
TreeNode curNode2=queue2.poll();
listInner.add(curNode2.val);//缓存当前层的值
if(curNode2.left!=null){
queue.offer(curNode2.left);//存储下一层的根节点
}
if(curNode2.right!=null){
queue.offer(curNode2.right);//存储下一层的根节点
}
}
listOuter.add(listInner);
}
return listOuter;
}
}
//使用一个队列即可
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution {
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
ArrayList<Integer> res=new ArrayList<Integer>();
if(root==null){
return res;
}
Queue<TreeNode> queue=new LinkedList<TreeNode>();
queue.add(root);
while(!queue.isEmpty()){
int size=queue.size();
for(int i=0;i<size;i++){
TreeNode curNode=queue.poll();
if(curNode.left!=null){
queue.add(curNode.left);
}
if(curNode.right!=null){
queue.add(curNode.right);
}
res.add(curNode.val);
}
}
return res;
}
}
二叉树的下一个节点(中等)
题目描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
分析
根据输入节点是否含有右子树来判断;
(1)如果输入参数含有右子树,则:找到右子树的最左边的叶子节点
(2)如果输入参数没有右子树,则:找向上寻他的祖宗节点,直到满足如下条件:当前节点必须是祖宗的左子节点。
/*
public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;
TreeLinkNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public TreeLinkNode GetNext(TreeLinkNode pNode){
//特判
if(pNode==null){
return null;
}
//如果存在右子树,则找到右子树最左边的节点;
if(pNode.right!=null){
//找右子树最深的左节点
TreeLinkNode node=pNode.right;
while(node.left!=null){
node=node.left;
}
return node;
}
//如果右子树子树为空,则一直循环向上找其祖宗节点,
//直到找到满足如下条件的祖宗节点为止:当前节点是该祖宗节点的左子节点
while(pNode.next!=null){
if(pNode.next.left==pNode){
return pNode.next;
}
pNode=pNode.next;
}
return null;
}
}
二叉排序树转换成双端链表(中等)
题目描述
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
例如二叉树为:
可以转化为:
思路分析:
先看下图,再解释:
关于二叉树的问题,基本上可以归纳为递归调用的问题,而递归调用的关键在于:让函数处理好当前节点 ,其他的就交给递归做就好了。这个问题可以这样分解:排序二叉树看成三个组成部分:根节点4,以2为根节点的左子树和以6为根节点的右子树。按照递归的思想,我们先处理好根节点,而至于另外两个子树则以相同的处理逻辑递归调用即可。同时,排序二叉树一定和中序遍历有关系,因此我们应该在中序遍历的过程中完成二叉树转化的过程,转换过程的另一个关键点在于:让左子树的最后一个节点lastNode的右指针指向当前节点,当前节点的左指针指向lastNode,当根节点4完成转化,后更新LastNode,即LastNode=Node;,由于这个lastNode依赖于左子树,所以应该先递归完成左子树,,此时再递归完成右子树(整体的框架为:中序遍历)
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
TreeNode lastNode=null;
public TreeNode Convert(TreeNode pRootOfTree){
if(pRootOfTree==null){
return null;
}
inorder(pRootOfTree);
//获取双端链表的头节点
while(lastNode.left!=null){
lastNode=lastNode.left;
}
return lastNode;
}
private void inorder(TreeNode node){
if(node==null){
return;
}
inorder(node.left);
//初始化,lastNode为当前节点,要不然会报空指针异常
if(lastNode==null){
lastNode=node;
}else{
node.left=lastNode;
lastNode.right=node;
lastNode=node;
}
inorder(node.right);
}
}
需要处理输入输出的版本
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
static TreeNode lastNode = null;
static TreeNode firstNode=null;
public static void main(String[] args) throws IOException{
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
String[] strs=br.readLine().trim().split(" ");
int nodeCount=Integer.parseInt(strs[0]);
int[][] arr = new int[nodeCount+1][2];//记录每个节点的左右子节点
//创建跟节点,并录入arr中
String[] nodes=br.readLine().trim().split(" ");
int rootVal=Integer.parseInt(nodes[0]);
int leftVal=Integer.parseInt(nodes[1]);
int rightVal=Integer.parseInt(nodes[2]);
arr[rootVal][0]=leftVal;
arr[rootVal][1]=rightVal;
for(int i=1;i<nodeCount;i++){
nodes=br.readLine().trim().split(" ");
arr[Integer.parseInt(nodes[0])][0]=Integer.parseInt(nodes[1]);
arr[Integer.parseInt(nodes[0])][1]=Integer.parseInt(nodes[2]);
}
TreeNode root=new TreeNode(rootVal);
createTree(root,arr);
inOrderHelper(root);
while(firstNode!=null){
System.out.print(firstNode.val+" ");
firstNode=firstNode.right;
}
}
private static class TreeNode {
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
// 根据输入流构建一课二叉树
private static void createTree(TreeNode root,int[][] arr) {
if(root==null){
return ;
}
int leftVal=arr[root.val][0];
int rightVal=arr[root.val][1];
if (leftVal != 0) {
TreeNode leftNode=new TreeNode(leftVal);
root.left=leftNode;
createTree(leftNode,arr);
}
if (rightVal != 0) {
TreeNode rightNode=new TreeNode(rightVal);
root.right=rightNode;
createTree(rightNode,arr);
}
}
private static void inOrderHelper(TreeNode root) {
if (root == null) {
return;
}
inOrderHelper(root.left);
// 构建双端链表的步骤
if (lastNode == null) {
lastNode = root;
firstNode=root;
} else {
root.left = lastNode;
lastNode.right = root;
lastNode = root;
}
inOrderHelper(root.right);
}
}
子树问题(简单)
问题描述:
输入两棵二叉树A,B,判断B是不是A的子结构。
树问题的处理方式:
归根结底,大部分二叉树的问题都可以使用递归的方式求解,求解过程可见树分成以下三个部分:根节点形成的当前树、根节点的左子树,根节点的右子树。我们递归的核心逻辑是:只处理当前的节点!只处理当前节点!只处理当前节点,由于其他节点的逻辑均一样,所以可以使用递归调用,分别传入root.left和root.right;
问题分析:
按照上述的思维方式,判断一树B是否为树A的子结构,需要解决两个大问题:
(1)如何判断两个树完全相同
- 特判:Bnull,则为true;B!=null && Anull 必为false;
- 使用递归的方式判断,两树的所有节点均相等(先当前节点,再递归左节点最后递归右节点)
(2)将树A分为三个部分:树A、左子树和右子树;接下来只要满足三个条件之一就能断定树B是树A的子树
- 树B和树A相同
- 树B和树A的左子树相同(递归)
- 树B和树A的右子树相同(递归)
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
//判断root2是否为root1的子树
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
//特判
if(root1==null || root2==null){
return false;
}
return isSame(root1,root2) || HasSubtree(root1.left,root2) || HasSubtree(root1.right,root2);
}
//判断两棵树是否相同
private boolean isSame(TreeNode root1,TreeNode root2){
//特判,如果B树为空,两者返回true
if(root2==null){
return true;
}
//B!==null && A==null,则B比不可能是A的子树,返回false
if(root1==null){
return false;
}
boolean res=true;
res=res && (root1.val==root2.val) && isSame(root1.left,root2.left) && isSame(root1.right,root2.right);
return res;
}
}
二叉搜素树的第k个节点
题目描述
给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。
思路分析:
单凡是BST问题,必然和中序遍历有着极大的联系,因为BST的中序遍历刚好就是各个节点从小到大的序列,这道的思路很简单,将这课树的中序遍历序列加入队列中(该过程在中序遍历的过程中完成),然后,读取到第K个节点即可 ;
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution {
TreeNode KthNode(TreeNode pRoot, int k){
if(k<1){
return null;
}
Queue<TreeNode> queue=new LinkedList<TreeNode>();
inorder(pRoot,queue);
if(queue.size()<k){
return null;
}
while(k>1){
queue.poll();
k--;
}
return queue.poll();
}
//中序遍历root树,并依次入队列
private void inorder(TreeNode root,Queue queue){
if(root==null){
return;
}
inorder(root.left,queue);
//将当前节点加入队列
queue.offer(root);
inorder(root.right,queue);
}
}
二叉树的序列化与反序列化
题目描述:
请实现两个函数,分别用来序列化和反序列化二叉树
[注] 二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。
题目分析:
可以使用前、中、后、层序等4中遍历方式来实现,注意在序列化和反序列化的过程中他们所使用的遍历方式保持一致即可,同时要遵循相同的约定
用到的java工具包:
StringBuilder、字符串中的常用方法;
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
private int index=-1;
String Serialize(TreeNode root) {
StringBuilder sb=new StringBuilder();
serializeHelper(root,sb);
return sb.toString();
}
//使用前序遍历进行序列化
private void serializeHelper(TreeNode root,StringBuilder sb){
//特判
if(root==null){
sb.append("#!");
return;
}
sb.append(root.val).append("!");
serializeHelper(root.left,sb);
serializeHelper(root.right,sb);
}
TreeNode Deserialize(String str) {
String[] strs=str.split("!");
return deserializeHelper(strs);
}
private TreeNode deserializeHelper(String[] strs){
index++;
//第一个节点不能是空节点
if(!strs[index].equals("#")){
TreeNode curNode=new TreeNode(Integer.parseInt(strs[index]));
curNode.left=deserializeHelper(strs);
curNode.right=deserializeHelper(strs);
return curNode;
}
return null;
}
}
Z字形打印树
题意描述:
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
思路分析:
该题的思路就是层序遍历,通过观察发现:当偶数层时,顺序加入list,当奇数层时,逆序加入list中,因此可以引入层数变量level 来记录当前是第几层。关于层序遍历的思路很简单:用一个队列缓冲,初始化时,将根节点入队列,计算当前队列中的size,每次从队列中取出size个的节点,这些节点则为当前层的节点,如果需要逆序打印,则有两种方式:
- list.add(0,node);每次从0索引开始加;
- 利用栈的特性,先入栈,然后再出栈
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.*;
public class Solution {
public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
ArrayList<ArrayList<Integer>> results=new ArrayList<ArrayList<Integer>>();
//特判
if(pRoot==null){
return results;
}
Queue<TreeNode> queue=new LinkedList<TreeNode>();
int level=0;
queue.offer(pRoot);
while(!queue.isEmpty()){
int size=queue.size();
ArrayList<Integer> list=new ArrayList<Integer>();
Stack<TreeNode> stack=new Stack<TreeNode>();
for(int i=0;i<size;i++){
TreeNode curNode=queue.poll();
if(level%2==0){
//偶数层,则顺序添加
list.add(curNode.val);
}else{//逆序放入list中
list.add(0,curNode.val);
//stack.add(curNode);
}
if(curNode.left!=null){
queue.offer(curNode.left);
}
if(curNode.right!=null){
queue.offer(curNode.right);
}
}
//while(!stack.isEmpty()){
//list.add(stack.pop().val);
//}
level++;
results.add(list);
}
return results;
}
}
对称二叉树
题目描述:
判断一棵树是否为对称二叉树,左右两节点围绕中轴线对称
解题思路:
先实现这么一个函数:如何判断两颗树是否对称,用递归的方式就是:如果两个根节点都为空,则必为对称,如果只有一个为空,则必然不对称,如果两个都不为空,需要判断两个根节点是否相等,然后使用相同的逻辑框架去递归判断左右子树,即:左子树的右节点要等于右子树的左节点,同时,右子树的左节点要等与左子树的右节点;,递归问题都是负责当前节点的问题;如何判断两颗树是否对称知道了,那么一颗树的对称不就是这棵树的左子树和右子树要对称吗?当然一定要有个特判:空树必然对称,只有一个子树的树必然不对称,其他情况再用上述函数进行判断。
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
boolean isSymmetrical(TreeNode pRoot){
if(pRoot==null){
return true;
}
if(pRoot.left==null && pRoot.right==null){
return true;
}
if(pRoot.left==null || pRoot.right==null){
return false;
}
return sysmmetrical(pRoot.left,pRoot.right);
}
//判断两颗树是否为对称的
private boolean sysmmetrical(TreeNode root1,TreeNode root2){
//两颗空树比对称
if(root1==null && root2==null){
return true;
}
//只有一个为空必然不对称
if(root1==null || root2==null){
return false;
}
//两个根节点不为空,则要求两个节点的值要相等;
return root1.val==root2.val && sysmmetrical(root1.left,root2.right) && sysmmetrical(root1.right,root2.left);
}
}
根节点到叶子节点和为sum的问题总结
是否存在路径
题目描述:
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
思路分析:
使用递归的方式来求解,当前节点的值如果是叶子节点并且其值为目标值,结束递归,找到;否则的话,在判断左子树到叶子节点的值是否为sum-target,同理,右子树也一样,两者是 || 的关系
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null)
return false;
int val = root.val;
if (root.left == null && root.right == null && val == sum)
return true;
return hasPathSum(root.left, sum - val) || hasPathSum(root.right, sum - val);
}
找到所有的路径
题目描述:
输入一颗二叉树的根节点和一个整数,按字典序打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。
题目分析:
这道题和上一道题,核心思想是一样的,只是需要用一个list来存储当前找到的路径,由于java中的参数是值传递,所以每次进入递归调用的时候需要想复制上一段的list。
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
import java.util.ArrayList;
public class Solution {
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
//记录总的路径
ArrayList<ArrayList<Integer>> res=new ArrayList<ArrayList<Integer>>();
//记录每一条路径
ArrayList<Integer> list=new ArrayList<Integer>();
findPath(root,target,list,res);
return res;
}
private void findPath(TreeNode root,int target,ArrayList<Integer> list, ArrayList<ArrayList<Integer>> res){
if(root==null){
return;
}
if(root.left==null && root.right==null && root.val==target){
list.add(root.val);
res.add(list);
return;
}
//想加入根节点的值,继续向下递归
list.add(root.val);
if(root.left!=null){
//这是上一list的基础上进行,所以复制
ArrayList<Integer> leftList=new ArrayList<Integer>(list);
findPath(root.left,target-root.val,leftList,res);
}
if(root.right!=null){
//复制
ArrayList<Integer> rightList=new ArrayList<Integer>(list);
findPath(root.right,target-root.val,rightList,res);
}
}
}
使用到的一个list复制方法: new ArrayList(list);复制list;
计算路径的总条数
题目描述:
输入一颗二叉树的根节点和一个整数,按字典序打印出二叉树中结点值的和为输入整数的所有路径的总数目。
思路分析:
和上题一样,直接看代码
class Solution {
public int pathSum(TreeNode root, int sum) {
//特判
if(root==null){
return 0;
}
//从根路径开始找,利用递归的方式遍历每一个节点
int rootSum=findPathFromNode(root,sum);
int leftSum=pathSum(root.left,sum);
int rightSum=pathSum(root.right,sum);
return rootSum+leftSum+rightSum;
}
//从指点节点Node中向下寻找路径,满足路径和=sum
private int findPathFromNode(TreeNode node,int sum){
if(node==null){
return 0;
}
//当前节点的值==sum,则路径为1
int pathSum=node.val==sum?1:0;
//向左递归查找左子树是否能找到路径
int leftSum=findPathFromNode(node.left,sum-node.val);
//向右递归查找是否能找到路径
int rightSum=findPathFromNode(node.right,sum-node.val);
return leftSum+rightSum+pathSum;
}
}
BST后序遍历序列
题目描述:
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
思路分析:
BST的后序序列有如下特点:最后一个元素为根节点,剩余元素中比根节点的小的在左半部分(左子树),比根节点大的在右半部分(右子树),关键点在于找到左右子树的分界点;两个fo循环就能解决;对于左右子树,同样用递归去求解,这里面要注意递归的结束条件:start>=root,为什么呢?当start==root时,只有根节点,必为true,当start>root时,只有左子树;
public class Solution {
int index=0;
public boolean VerifySquenceOfBST(int [] sequence) {
if(sequence==null || sequence.length==0){
return false;
}
return verifyHelper(sequence,0,sequence.length-1);
}
private boolean verifyHelper(int[] sequence,int start,int root){
//只有根节点
if(start>=root){
return true;
}
int key=sequence[root];//数组最后一个值一定为根节点的值
//找到左、右子树的分界点
//i的左边全小于key,i的右边全大于key
int i=0;
for(i=start;i<root;i++){//[0,root)
if(sequence[i]>key){
break;
}
}
for(int j=i;j<root;j++){
if(sequence[j]<key){
return false;
}
}
//找到了i;左子树为[0,i-1],右子树为[i,root-1]
//使用递归的方式
return verifyHelper(sequence,start,i-1) && verifyHelper(sequence,i,root-1);
}
}
打印二叉树的边界节点
题目描述:
按照边界节点的定义,逆时针打印边界点。
标准一:
1,根节点为边界节点。
2,叶节点为边界节点。
3,如果节点在其所在的层中是最左的或最右的,那么该节点也是边界节点。
标准二:
1,根节点为边界节点。
2,叶节点为边界节点。
3,树左边界延伸下去的路径为边界节点。
4,树右边界延伸下去的路径为边界节点。
解题思路:
标准一:设置一个levelLR用于存储每一层的最左节点和最右节点,在前序的遍历过程中填充这个levelLR,还要实现一个打印不在这个levelLR中叶子节点的方法,依次逆时针打印边界节点的步骤可以理解为:从上到下,遍历levlelLR的左节点(levelLR[i][0]);打印最每一层的叶子节点,且该节点不能再levelLR中(因为要去重);从下到上,遍历levelLR的右节点(levelLR[j][1]),同时也遍历左节点去重。
标准二:逆时针打印可以抽象为:根-->左子树---底边叶子节点;底边叶子节点--->右子树,根节点;当打印左边的子树时,只考虑叶子节点,或者(该节点是左子孩子,且他没有右兄弟);当打印右边子树的时候,使用后序遍历的方式,考虑叶子节点或者(该节点是右孩子,且它没有左兄弟);整体的步骤:先左边后右边,或者,当只有左边和只有右边的时候,就打一边;
import java.util.*;
import java.io.*;
public class Main{
public static void main(String[] args) throws IOException{
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
String[] strs=br.readLine().trim().split(" ");
int count =Integer.parseInt( strs[0]);
int rootVal=Integer.parseInt(strs[1]);
int[][] arr=new int[count+1][2];
for(int i=0;i<count;i++){
String[] nodes=br.readLine().trim().split(" ");
arr[Integer.parseInt(nodes[0])][0]=Integer.parseInt(nodes[1]);
arr[Integer.parseInt(nodes[0])][1]=Integer.parseInt(nodes[2]);
}
//创建根节点
TreeNode root=new TreeNode(rootVal);
createTree(root,arr);
int height=getHeight(root);
TreeNode[][] levelLR=new TreeNode[height][2];
setLevelLR(root,levelLR,0);
printEdge1(root,levelLR);
System.out.println();
printEdge2(root);
}
private static class TreeNode{
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val){
this.val=val;
}
}
private static void createTree(TreeNode root,int[][] arr){
if(root==null){
return;
}
int i=root.val;
int leftVal=arr[i][0];
int rightVal=arr[i][1];
if(leftVal!=0){
TreeNode left=new TreeNode(leftVal);
root.left=left;
createTree(root.left,arr);
}
if(rightVal!=0){
TreeNode right=new TreeNode(rightVal);
root.right=right;
createTree(root.right,arr);
}
}
private static int getHeight(TreeNode root){
if(root==null){
return 0;
}
return 1+Math.max(getHeight(root.left),getHeight(root.right));
}
/** levelLR[levelCount][2]是一个存储每一层的左、右节点的数组;
*levelLR[i][0]:第i层最左边的节点,第i层最右边的节点。
**/
private static void setLevelLR(TreeNode root,TreeNode[][] levelLR,int level){
if(root==null){
return;
}
levelLR[level][0]=levelLR[level][0]==null?root:levelLR[level][0];//最左节点
levelLR[level][1]=root;//最右节点;
//递归下一层,先左后右边
setLevelLR(root.left,levelLR,level+1);
setLevelLR(root.right,levelLR,level+1);
}
//打印叶子节点,但是该叶子节点不能在levelLR中,因为levelLR中的节点已经被打印了
private static void printLeafNotInLevelLR(TreeNode root,TreeNode[][] levelLR,int level){
if(root==null){
return ;
}
//不在levelLR中的叶子节点
if(root.left==null && root.right==null && root!=levelLR[level][0] && root!=levelLR[level][1]){
System.out.print(root.val+" ");
}
//递归所以的叶子节点(逆时针,为从左到右)
printLeafNotInLevelLR(root.left,levelLR,level+1);
printLeafNotInLevelLR(root.right,levelLR,level+1);
}
//打印边界节点
private static void printEdge1(TreeNode root,TreeNode[][] levelLR){
if(root==null){
return;
}
//先打印最左边边界
for(int i=0;i<levelLR.length;i++){
System.out.print(levelLR[i][0].val+" ");
}
//最下边的叶子节点(除了左、右边界)
printLeafNotInLevelLR(root,levelLR,0);
//打印最右边边界
for(int i=levelLR.length-1;i>=0;i--){
if(levelLR[i][0]!=levelLR[i][1]){
System.out.print(levelLR[i][1].val+" ");
}
}
}
//********************************打印标准2***************************************/
private static void printEdge2(TreeNode root){
if(root==null){
return;
}
//先打印根节点
System.out.print(root.val+" ");
if(root.left!=null && root.right!=null){
//打印处于左边界及其延申
printLeft(root.left,true);
//打印右边界及其延申
printRight(root.right,true);
}else{
//有左孩子,则递归左,否则递归右
printEdge2(root.left!=null?root.left:root.right);
}
}
//打印左边,先根、左、右
private static void printLeft(TreeNode root,boolean print){
if(root==null){
return ;
}
if(print || (root.left==null && root.right==null)){
//只有叶子节点或者print为true
System.out.print(root.val+" ");
}
printLeft(root.left,print);
//当前节点处右节点位置,只有左节点为空时,才考虑打印右节点(该节点还必须是叶子节点);
printLeft(root.right,print && root.left==null?true:false);
}
//打印右边界,左,右,根;
private static void printRight(TreeNode root,boolean print){
if(root==null){
return;
}
//当前节点处于左节点的时候,只有当右节点为空的时候,考虑打印该节点(并且该节点还必须是叶子节点)
printRight(root.left,print && root.right==null?true:false);
printRight(root.right,print);
//打印根节点
if(print || (root.left==null && root.right==null)){
System.out.print(root.val+" ");
}
}
}
动态规划
动态规划问题套路
(1)确定状态(问题描述中的自变量)和选择(可能改变状态的操作)
(2)定义dp表
(3)根据选择,思考状态转移的逻辑关系
剪绳子问题
问题描述
有根长度为m的绳子,把绳子剪成整数长的n段,n<=m,求出每段长度的最大乘积。
解题思路
使用动态规划的方法求解,按照动态规划的套路开始:
状态: 绳子的长度
定义dp: dp[i]表示长度为i的绳子的分段后的最大乘积;
选择: 剪和不剪; 不剪切的则为dp[i],自底而上推导:j*dp[i-j]表示剪切后,第一段长度为j,则剩余长度的最大乘积为dp[i-j],即剪切时候根据第一段长度的而有很多种情况,我们需要一一列举出来;
base_case: 显然,当i<=4时,dp[i]=i;
import java.util.*;
public class Solution {
public int cutRope(int target) {
//特判,1=1*1
if(target==2){
return 1;
}
if(target==3){
return 2;
}
int[] dp=new int[target+1];
//base_case
for(int i=1;i<=4;i++){
dp[i]=i;
}
//dp[5]=max(dp[5],j*dp[i-j])--->dp[6]---->dp[target]
for(int i=5;i<=target;i++){
//j表示剪切后,第一段绳子的长度
for(int j=1;j<i;j++){
//有两种选择:
//不剪dp[i]
//剪---怎么剪切,按第一段长度依次列举出来,j*dp[i-j]
dp[i]=Math.max(dp[i],j*dp[i-j]);
}
}
return dp[target];
}
}