剑指offer-附加

题目-求1+2+3+...+n

求1+2+3+...+n,要求不能使用乘除法、forwhileifelseswitch、case等关键字及条件判断语句(A?B:C)。 

思路

由于需要用到的就是一个递归,当n>0时,将后面的数字进行一个递归的相加。当输入0时,即n==0,输出0,直接不进行递归。

这个题的关键要求在于不能使用xxxxxx一系列语句。

那么看起来唯一能用到的只有位运算、加减。

用到逻辑与的短路特征:即如($b=false)&&($a=true),当&&前面的语句不成立(false)时,&&后面的语句不会执行;以此来实现递归终止。

(ps)逻辑或的短路特征:如($b=true) || ($a=false),当||前面的语句成立(true)时,||后面的语句不会执行。

即当n>0时,执行sum+=Sum_Solution(n-1)进行递归的累加;当n==0时,执行&&前面语句的判断(n>0),为false,然后不对sum做什么修改处理,直接就是返回第一句的sum=n=0。

解法

public class Solution {
    public int Sum_Solution(int n) {
          int sum = n;
          boolean ans = (n>0)&&((sum+=Sum_Solution(n-1))>0);
          return sum;     
    }
}

 

题目-不用加减乘除做加法 

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 

思路

那么这道题的要求也是使用位运算,我们来回顾一下常用的位运算。

1.移位运算:

  左移<<:向左移动,右边低位补0.左移1位相当于乘以2

  e.g. 3<<2:00000011 ---> 00001100(12)

  无符号右移>>>:向右移动,右边舍弃,左边补0

  有符号右移>>:向右移动,右边舍弃,左边补的值取决于原来的最高位。原来是1就补1,原来是0就补0.右移1位相当于除以2.

  e.g. 3>>1:00000011 --->  00000001(1)

2.逻辑运算:

  与 & :两个位都为1,才为1

  或 | :只有一位为1,就为1

  取反 ~ :1变成0,0变成1

  按位异或  ^ :相异为真,相同为假

3.应用场景:

  1)判断奇偶:(1&i)==1,此时i的最低位为1,偶数;(1&j)==1,此时j的最低位为1,奇数。

解法

public class Solution {
    public int Add(int num1,int num2) {
        int sum, carry;
        do{
            //各位相加,不计进位。二进制每位相加相当于各位做异或操作
            sum = num1^num2;
            //记下进位。两个数各位做与运算,再左移一位。
            carry = (num1 & num2)<<1;
            num1 = sum;
            num2 = carry;
        }while(num2!=0);//直到进位值为0
        return num1;
    }
}

 

题目-把字符串转换成整数

将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0 

思路

1.这个题需要注意到的是数据的边界,可能会溢出。int范围为[-2147483648,2147483647],可以用Integer.MAX_VALUE和Integer.MIN_VALUE来表示。

2.边界条件:数据溢出;空字符串;正负号情况;非法字符

3.库函数:int atoi(const char *str );//将字符串转换为整型

atol();//将字符串转换为长整型值。

char *itoa( int value, char *string,int radix);//整数转换为字符串。value:欲转换的数据;string:目标字符串的地址;radix:转换后的进制数,可以是10进制、16进制等。

解法

public class Solution {
    public int StrToInt(String str) {
        if(str==null || str.length()==0)
            return 0;
        //判断是否为负数
        boolean isMinus = str.charAt(0)=='-';
        //设置为long类型,避免溢出
        long ret = 0;
        for(int i=0; i<str.length(); i++){
            char c = str.charAt(i);
            //符号判定
            if(i==0 && (c=='+' || c=='-'))
                continue;
            //如果不是数字,非法输入
            if(c<'0' || c>'9')
                return 0;
            //将字符拼接成数字
            ret = ret*10+(c-'0');
        }
        ret=isMinus ? -ret : ret;
        //溢出就返回0,用long类型的res来比较,
        //如果定义为int res,那再比较就没有意义了,int范围为[-2147483648,2147483647]
        if(ret>Integer.MAX_VALUE|| ret<Integer.MIN_VALUE)
            return 0;
        return (int)ret;
    }
}

 

题目-数组中重复的数字

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 

思路

要求时间复杂度为O(N),空间复杂度为O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。

那么对于这种数组元素在固定范围的问题。考虑用将值为i的元素调整到第i个位置来进行求解。

这样的话,如果当将第i个元素调整到第i位时,我们发现某个位置i已经有这个元素i,那么就说明重复。

并且只是要求返回任意重复的一个,赋值duplication[0]

解法

public class Solution {
    // Parameters:
    //    numbers:     an array of integers
    //    length:      the length of array numbers
    //    duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
    //                  Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
    //    这里要特别注意~返回任意重复的一个,赋值duplication[0]
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        if(numbers==null || length==0)
            return false;
        //将值为1的元素调整到第i个位置上求解
        for(int i=0; i<length; i++){
            while(numbers[i]!=i){
                //比如在第二个位置,已经有一个2的值了,那么就记录重复
                if(numbers[i]==numbers[numbers[i]]){
                    duplication[0]=numbers[i];
                    return true;
                }
                swap(numbers, i, numbers[i]);
            }
        }
        return false;
    }
    private void swap(int numbers[], int i, int j){
        int temp = numbers[i];
        numbers[i] = numbers[j];
        numbers[j] = temp;
    }
}

 

题目-构建乘积数组

给定一个数组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];) 

思路

要求不能使用除法,最简单的思路当然就是连乘,但是时间复杂度为O(N^2),效率低。

那我们将每个B[i] 的计算式列出来,考虑B[i] 之间的联系。

列出上表,可以发现B[i] 其实不同之处就是在于第i位是为1,那么可以将B[i]的计算以哪一位为1为分割线。

B[i]的左半部分与B[i-1]有关,即B[i]的左半部分=B[i-1]的左半部分 * A[i-1]

B[i]的右半部分与B[i+1]有关,即B[i]的右半部分=B[i+1]的右半部分 * A[i+1]

然后分别计算左右半部分,再乘起来。

解法

import java.util.ArrayList;
public class Solution {
    public int[] multiply(int[] A) {
        int n = A.length;
        int[] B = new int[n];
        B[0] = 1;
        //B[i]的左半部分和B[i-1]有关
        for(int i=1; i<n; i++){
            B[i] = B[i-1]*A[i-1];
        }
        //temp表示右半部分的乘积
        int temp=1;
        //B[i]的右半部分和B[i+1]有关
        for(int i=n-2; i>=0; i--){
            temp *= A[i+1];
            B[i] *= temp;
        }
        return B;
    }
}

 

题目-正则表达式匹配

请实现一个函数用来匹配包括'.''*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a""ab*ac*a"匹配,但是与"aa.a""ab*a"均不匹配 

思路

比较值得注意的就是*的处理。因为它可能表示前面的字符出现任意次(包含0次)

出现的情况可能有:

  1.同时匹配到末尾,就匹配成功;

  2.模式串到了末尾,但是字符串还没有到末尾。那说明字符串后面的那一部分没办法被匹配,匹配失败;

  3.模式串中的下一个字符是*:

    1)如果当前字符不匹配,将模式串后移两位,继续匹配。

     如模式串为a*b,字符串为bbb.

     后移两位是因为,如果当前不匹配,那么只能是说明*表示的是当前字符出现0次,相当于a*被忽略。那么模式串就应该跳过这两个字符,从*后面的b开始匹配字符串。

    2)如果当前字符匹配 或者 模式串的当前字符为 ‘.’(由于模式中的字符'.'表示任意一个字符),那么根据*的匹配方式,那么可能出现:

     如模式串为a*b,字符串为aaa.

     若*表示前面字符出现0次,那么模式串后移两位,字符串不动;

     若*表示前面字符出现1次,那么模式串后移两位,字符串后移一位。相当于a*看成一个整体,与字符串的第一个a,来匹配。匹配完后,就都往后移,匹配下一位。

     若*表示前面字符出现多次,那么模式串不动,字符串后移一位。相当于*也就是a,与字符串的第二个a去匹配。因为*不能表示直接来匹配,只能依据它前面依附的字符来匹配。

     将这三种可能的匹配方式,来进行或处理。因为*肯定是出现在这三种情况里。     

  4.模式串中下一个字符不是*:

    1)当前字符匹配 或者 模式串的当前字符为 ‘.’ ,那么后移匹配下一字符;

    2)当前字符不匹配,则匹配失败。

同时还需要在java里,还要时刻注意检查数组是否越界。(比如需要去找,字符串和模式串是否匹配到末尾。因为在匹配过程中会有需要字串后移的操作)    

解法

public class Solution {
    public boolean match(char[] str, char[] pattern)
    {
        //空串则返回false
        if(str==null || pattern==null){
            return false;
        }
        //递归地匹配
        return matchCore(str, 0, pattern, 0);
    }
    //设置两个指针,分别指向字符串和模式串的第一个字符
    private boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex){
        int m=str.length, n=pattern.length;
        //如果同时匹配到末尾,那么则匹配成功
        if(strIndex==m && patternIndex==n){
            return true;
        }
        //如果模式串先扫描到末尾,则匹配失败
        if(strIndex!=m && patternIndex==n){
            return false;
        }
        //如果下一个字符是"*"
        if(patternIndex+1<n && pattern[patternIndex+1]=='*'){
            //字符串和模式串当前字符匹配
            if(strIndex!=m && (str[strIndex]==pattern[patternIndex] || pattern[patternIndex] == '.')){
                return matchCore(str, strIndex, pattern, patternIndex+2)|| //‘*’前的字符出现0次,那么pattern就要多走*和*前面的字符
                    matchCore(str, strIndex+1, pattern, patternIndex+1)|| //‘*’前的字符只出现1次,那么pattern和str都进入下一步的匹配
                    matchCore(str, strIndex+1, pattern, patternIndex);  //‘*’前的字符出现多次
            }
            else{
                //如果当前字符串不匹配,则认为‘*’前的字符出现0次
                //因为只有模式串当前字符不出现时,才可能字符串当前字符和模式串下一字符匹配
                return matchCore(str, strIndex, pattern, patternIndex+2);
            }
        }
        //如果下一个字符不是'*'
        //当前字符匹配
        if(strIndex!=m && (str[strIndex]==pattern[patternIndex] || pattern[patternIndex]=='.')){
            return matchCore(str, strIndex+1, pattern, patternIndex+1);
        }
        else{//当前字符不匹配
            return false;
        }
    }
}

 

题目-表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416""-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5""12e+4.3"都不是。 

思路

这里是使用正则匹配。给出一些常用的正则表示:

[xyz]:字符集,匹配括号中包含的任一字符。

[^xyz]:反向字符集,匹配括号中未包含的任一字符。

?:零次或一次匹配前面的字符或子表达式。

+:一次或多次匹配前面的字符或子表达式。

*:零次或多次匹配前面的字符或子表达式。

^:匹配输入字符串开始的位置。

$:匹配输入字符串结尾的位置。

\:将下一字符标记为特殊字符、文本、反向引用或八进制转义符。

\d:数字字符匹配,[0-9]。(但是java中\是转移字符前导符,因此在字符串中书写\必须写成\\

\D:非数字匹配,[^0-9]。

\w:匹配任何字类字符,[A-Za-z0-9]。

常用的函数:

public boolean matches();//尝试将整个区域与模式匹配;

public boolean find();//从当前位置开始匹配;

public boolean lookingAt();//尝试将从区域开头开始的输入序列 与 该模式匹配。从第一个字符开始匹配,但是不要求整句都匹配。

解法

public class Solution {
    public boolean isNumeric(char[] str) {
        if(str==null || str.length==0)
            return false;
        return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
    }
}

 

题目-字符流中第一个不重复的字符

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"

思路

时间复杂度O(1),空间复杂度O(n)

1.构建一个大小为128的数组,统计每个字符ch出现的次数;

2.构建一个队列,记录所有第一次出现的ch字符。如果当前字符出现2次,那么弹出;

3.此时队列中全都是只出现一次的字符。如果是空,那么就输出“#”;如果不是空,那么弹出队首。

解法

import java.util.Queue;
import java.util.LinkedList;
public class Solution {
    private int[] cnts = new int[256];
    private Queue<Character> queue = new LinkedList<>();
    //Insert one char from stringstream
    public void Insert(char ch)
    {
        //记录各元素的出现次数
        cnts[ch]++;
        //在队列中记录所有出现过的元素
        queue.add(ch);
        //如果这个元素的次数大于2,那么弹出
        while(!queue.isEmpty() && cnts[queue.peek()]>1){
            //返回第一个元素,并在队列中删除
            queue.poll();
        }
    }
  //return the first appearence once char in current stringstream
    public char FirstAppearingOnce()
    {
        //若当前字符流没有存在出现一次的字符,返回#
        return queue.isEmpty() ? '#' : queue.peek();
    }
}

 

题目-链表中环的入口结点

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。 

思路 

设置快慢指针,从表头同时出发。快指针每次走两步,慢指针每次走一步。

1.如果有环,快慢指针一定会相遇在环中某点。(由于有环,fast先进入环,那么low进入环,可以将两者看成是一个追赶的过程)

2.两个指针分别从链表头和相遇点继续出发,每次走一步,最终一定会相遇在环入口。

设置AC=a,CB=b,BC=c。

相遇时,快指针的路程=a+(b+c)*k+b;慢指针的路程=a+b.

而快指针的速度=2*慢指针的速度。因此2*(a+b)=a+(b+c)*k+b,化简得到a=(k-1)(b+c)+c,即 链表头到环入口的距离=相遇点到环入口的距离+(k-1)圈环长度。

得证。

解法

/*
 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, fast=pHead;
        //因为存在环,两个指针必定相遇在环中的某个节点
        do{
            fast=fast.next.next;
            slow=slow.next;
        }while(slow!=fast);
        //让fast从头开始移动,速度变为一次移动一个节点
        fast=pHead;
        while(slow!=fast){
            slow=slow.next;
            fast=fast.next;
        }
        return slow;
    }
}

 

题目-删除链表中重复的结点

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5 

思路

使用递归

1)如果当前结点是重复结点,那么删除这个重复结点,从第一个与当前结点不同的结点开始递归。

2)如果当前结点不重复,那么直接从下一结点开始递归。

解法

/*
 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 next = pHead.next;
        //如果出现重复结点,删除重复结点且不保留
        if(next.val == pHead.val){
            while(next!=null && next.val==pHead.val){
                next = next.next;
            }
            return deleteDuplication(next);
        }
        //如果没有重复结点,那么就判断下一个结点
        else{
            pHead.next = deleteDuplication(pHead.next);
            return pHead;
        }
    }
}

 

题目-二叉树的下一个结点

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

思路

根据中序遍历的规则:左根右。得到遍历的规律

1.如果有右子树,那么下一个结点就是右子树最左结点;

2.如果没有右子树,那么下一结点是:a)父结点,当前结点为父结点的左孩子时;b)向上找到第一个左子树指向的树,比如J,其下一节点是父结点的父结点的父结点,直到当前结点是这个父结点的左子树部分。因为左根右,当前结点是右结点的时候,说明已经遍历到这棵树的最后一个结点,应该开始遍历另外半棵树。

解法

/*
public class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null; //next为指向父结点的指针
 
    TreeLinkNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode)
    {
        //中序遍历:左根右
        //如果一个结点的右子树不为空,那么该结点的下一个结点是右子树的最左结点
        if(pNode.right != null){
            TreeLinkNode node = pNode.right;
            while(node.left != null){
                node = node.left;
            }
            return node;
        }
        //否则,向上找第一个左链接指向的树 包含 该结点的祖先结点
        else{
            while(pNode.next != null){
                TreeLinkNode parent = pNode.next;
                if(parent.left == pNode){
                    return parent;
                }
                pNode = pNode.next;
            }
        }
        return null;
    }
}

 

题目-对称的二叉树

请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 

思路

使用递归,只是需要不断判断结点的left和right是否对称;

即左右结点的值要相等&&对称子树left.left与right.right、left.right与right.left也要对称。

解法

/*
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;
        }
        return isSymmetrical(pRoot.left, pRoot.right);
    }
     
    boolean isSymmetrical(TreeNode t1, TreeNode t2){
        if(t1 == null && t2 == null)
            return true;
        if(t1 == null || t2 == null)
            return false;
        if(t1.val != t2.val)
            return false;
        return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left);
    }
}

 

题目-按之字形顺序打印二叉树

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

思路

1.将每层数据存入ArrayList,当偶数层时通过Collections.reverse(list);翻转list。但是其实我们给出来的这种方法使用reverse,处理海量数据时,效率太低。

2.可以利用LinkedList实现的是双向链表的特点,做成双向遍历。

  使用queue.iterator();//从前往后遍历

  queue.descendingIterator();//从后往前遍历

  并且需要在queue中添加null做层分隔符

解法

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Collections;
/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(pRoot);
        //用变量记录奇数层还是偶数层,是否反向
        boolean reverse = false;
        while(!queue.isEmpty()){
            ArrayList<Integer> list = new ArrayList<>();
            int cnt = queue.size();
            //如果队列不为空,就循环取值
            while(cnt-- > 0){
                TreeNode node = queue.poll();
                if(node == null)
                    continue;
                list.add(node.val);
                queue.add(node.left);
                queue.add(node.right);
            }
            //如果是从右到左,就反转list
            if(reverse == true)
                Collections.reverse(list);
            //反向
            reverse = !reverse;
            if(list.size() != 0){
                ret.add(list);
            }
        }
        return ret;
    }
 
}

 

题目-把二叉树打印成多行

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。 

思路

这个题与上一题“按照之字形打印二叉树”类似。

只是去掉了需要翻转的部分。

构建一个队列来存储二叉树,然后将一个结点从队列中取出并存入list的同时,将结点的左右结点添加到队列。不断做输出。

解法

import java.util.ArrayList;
import java.util.Queue;
import java.util.LinkedList;
/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
    }
 
}
*/
public class Solution {
    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
        //构造队列来存储二叉树
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(pRoot);
        while(!queue.isEmpty()){
            ArrayList<Integer> list = new ArrayList<>();
            int cnt = queue.size();
            while(cnt-- > 0){
                TreeNode node = queue.poll();
                if(node == null)
                    continue;
                //将当前结点存储到list中,待输出
                list.add(node.val);
                //加入当前结点的左右子树
                queue.add(node.left);
                queue.add(node.right);
            }
            if(list.size() != 0)
                ret.add(list);
        }
        return ret;
    }
     
}

 

题目-序列化二叉树

请实现两个函数,分别用来序列化和反序列化二叉树 

二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过 某种符号表示空节点(#),以 ! 表示一个结点值的结束(value!)。

二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。

思路 

1.序列化:就是使用一些遍历规则来将二叉树遍历为字符串,比如前序遍历“根左右”。就按照这个顺序读结点的值、递归左结点、递归右结点。

2.反序列化:按照遍历顺序,重构二叉树。

  根据字符串序列,由于序列化时,不同结点之间有分隔符,通过分隔符,获取每个结点。

  将该结点的字符截取出来,然后截断原字符。根据结点的值构建二叉树、递归重构左结点、递归重构右结点。

3.public String substring(int beginIndex);//返回一个新字符串,它是此字符串的一个子字符串。该子字符串始于指定索引处的字符,一直到此字符串末尾。

public String substring(int beginIndex, int endIndex);//返回一个新字符串,它是此字符串的一个子字符串。该子字符串从指定的 beginIndex 处开始, endIndex:到指定的 endIndex-1处结束。

解法 

/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        if(root==null){
            return "null,";
        }
        String res=root.val+",";
        res+=serialize(root.left);
        res+=serialize(root.right);
        return res;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        String str[]=data.split(",");
        Queue<String> queue=new LinkedList<String>();
        for(int i=0;i<str.length;i++){
            queue.offer(str[i]);
        }
        return helper(queue); 
    }

    public TreeNode helper(Queue<String> queue){
        String val=queue.poll();
        if(val.equals("null")){
            return null;
        }
        TreeNode root=new TreeNode(Integer.valueOf(val));
        root.left=helper(queue);
        root.right=helper(queue);
        return root;
    }
}
public class Solution {
    String Serialize(TreeNode root) {
        //序列化时通过 某种符号表示空节点(#)
        if(root == null)
            return "#";
        return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
    }
    private String deserializeStr;
TreeNode Deserialize(String str) { deserializeStr
= str; return Deserialize(); } TreeNode Deserialize(){ if(deserializeStr.length() == 0) return null; //获取一个结点 int index = deserializeStr.indexOf(" "); //该子字符串从指定的 beginIndex 处开始, endIndex:到指定的 endIndex-1处结束。 String node = index==-1 ? deserializeStr : deserializeStr.substring(0, index); //将字符串截掉已经存入node的那一部分。substring该子字符串始于指定索引处的字符,一直到此字符串末尾。 deserializeStr = index==-1 ? deserializeStr : deserializeStr.substring(index+1); //"#"为空结点 if(node.equals("#")){ return null; } int val = Integer.valueOf(node); TreeNode t = new TreeNode(val); t.left = Deserialize(); t.right = Deserialize(); return t; } }

 

题目-二叉搜索树的第k个结点

给定一棵二叉搜索树,请找出其中的第k小的结点。例如,(5372468)中,按结点数值大小顺序第三小结点的值为4。

思路 

二叉搜索树(二叉查找树、二叉排序树):根节点的值大于其左子树中任意一个节点的值,小于其右节点中任意一节点的值,这一规则适用于二叉查找树中的每一个节点。

根据二叉搜索树的特点,我们可以发现其中序遍历(左根右)是有序的。即按照中序遍历的顺序,找到的第k个结点就是结果。

解法

/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    //二叉搜索树的特性:根结点的值大于其左子树中任意一个结点的值,小于其右子树中任意一结点的值。
    //因此中序遍历(左根右)是有序的
    private TreeNode ret;
    private int cnt = 0;
    TreeNode KthNode(TreeNode pRoot, int k)
    {
        inOrder(pRoot, k);
        return ret;
    }
    private void inOrder(TreeNode pRoot, int k){
        if(pRoot == null || cnt >= k){
            return;
        }
//左 inOrder(pRoot.left, k);
//根,标记第k个结点 cnt++; if(cnt == k){ ret = pRoot; }
   //右 inOrder(pRoot.right, k); } }

 

题目-数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

传入的数据为:[5,2,3,4,1,6,7,0,8],那么按照要求,输出是"5.00 3.50 3.00 3.50 3.00 3.50 4.00 3.50 4.00 "。

思路

1.也就是不断根据获取的数据流,获得当前数据的中位数。注意中位数是需要排序的。

2.那么我们可以通过优先队列PriorityQueue来设置大顶堆和小顶堆。

其中大顶堆用来存较小的数,从大到小排序;小顶堆用来村较大的数,从小到大排序。

这样的话中位数也就是大顶堆的根结点(较小数中的最大) 和 小顶堆的根结点(较大数中的最小)的平均数。也就是中位数依据的 所有数值排序后中间的两个数

3.构造两个堆的时候,需要保证小顶堆的元素都大于等于大顶堆的元素&&需要保证两个堆的数据处于平衡状态(元素个数相差不超过1)

每个堆加一个的顺序。

当数目为偶数时,将这个值插入大顶堆中,再从大顶堆中poll出根结点(最大值),插入到小顶堆;

当数目为奇数时,将这个值插入小顶堆中,再从小顶堆中poll出根结点(最小值),插入到大顶堆;

因此取中位数时,若当前个数为偶数,那么就是取大顶堆和小顶堆的根结点的平均值;若当前个数为奇数时,就是取小顶堆的根结点。

4.队列遵循先进先出原则,但是有时需要在队列中基于优先级处理对象。PriorityQueue是基于优先堆的一个无界队列,这个优先队列中的元素可以默认自然排序(从小到大)或者通过提供的Comparator(比较器)在队列实例化的时排序。优先队列不允许空值,且队列的大小不受限制,在创建时可以指定初始大小(如下面定义maxHeap时给出的11)。当我们向优先队列增加元素的时候,队列大小会自动增加。

PriorityQueue是非线程安全的,所以Java提供了PriorityBlockingQueue(实现BlockingQueue接口)用于Java多线程环境。

解法

import java.util.Comparator;
import java.util.PriorityQueue;
public class Solution {
    //当前数据流读入的元素个数
    public int count = 0;
    //用两个堆来保存数据
    //小顶堆,存储右半边元素,右半边元素都大于左半边
    public PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    //大顶堆,存储左半边元素。
    public PriorityQueue<Integer> maxHeap = new PriorityQueue<>(11,
            new Comparator<Integer>(){
                public int compare(Integer o1, Integer o2){
                    return o2 - o1;
                }
            });
    public void Insert(Integer num) {
        count++;
        //插入过程,需要保证两个堆的数据处于平衡状态(元素个数相差不超过1)
        if(count % 2 ==0){//偶数
            //因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素大
            //因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点(堆顶元素即为最大值),插入右半边)
            //即偶数情况 都插入到右半边
            if(!maxHeap.isEmpty() && num < maxHeap.peek()){
                maxHeap.add(num);
                num = maxHeap.poll();              
            }
            minHeap.add(num);  
        }
        else{//奇数
            //将新加入的元素加入小顶堆
           if(!minHeap.isEmpty() && num > minHeap.peek()){
                minHeap.add(num);
                num = minHeap.poll();              
            }
            maxHeap.add(num); 
        }
    }
 
    public Double GetMedian() {
        if(maxHeap.size() == minHeap.size())
            return (minHeap.peek() + maxHeap.peek()) / 2.0;
        else if(maxHeap.size() > minHeap.size())
            return maxHeap.peek()*1.0;
        else
            return minHeap.peek()*1.0;
        /*if(count % 2 == 0){//总量是偶数
            return (minHeap.poll() + maxHeap.poll()) / 2.0;
        }
        else{//总量是奇数
            return (double)minHeap.poll();
        }*/
    }
 
 
}

 

题目-滑动窗口的最大值 

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个。 

{[2,3,4],2,6,2,5,1},

{2,[3,4,2],6,2,5,1},

{2,3,[4,2,6],2,5,1},

{2,3,4,[2,6,2],5,1},

{2,3,4,2,[6,2,5],1},

{2,3,4,2,6,[2,5,1]}

思路

1.由于题目要求输出滑动窗口中数值最大值。同样使用java的PriorityQueue,使用大顶堆。通过滑动窗口里的值来维护这个大顶堆。

滑动窗口滑动时,为了维护滑动窗口值组成的大顶堆,将当前元素加入到大顶堆,堆顶元素就是最大值。将滑动窗口最左边的值去掉。

最后把该元素添加到Arraylist中待输出。

2.Queue的用法:

  offer()&add():add()在失败时会抛出异常。

  poll()&remove():返回第一个元素,并在队列中删除。remove()在失败时会抛出异常。

  peek()&element():返回第一个元素.element()在队列为空时,会抛出异常。

解法

import java.util.*;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size)
    {
        ArrayList<Integer> ret = new ArrayList<>();
        if(size > num.length || size < 1){
            return ret;
        }
        //构建大顶堆.
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(11,
               new Comparator<Integer>(){
                   public int compare(Integer o1, Integer o2){
                       return o2 - o1;
                   }
               });
        //最初添加,滑动窗口滑动时,将当前元素加入大顶堆中,堆顶元素即是最大值。把头三个元素加入到maxHeap中,并且输出最大元素到Arraylist
        for(int i = 0; i < size ; i++){
            maxHeap.add(num[i]);
        }
        //同时还要判断当前元素是否存在于当前窗口中,不存在的话弹出,最后将该元素添加到Arraylist
        ret.add(maxHeap.peek());
        //窗口开始滑动。维护一个大小为size的大顶堆
        for(int i=0, j=i+size; j < num.length; i++, j++){
            //按照窗口,添加元素。maxHeap中始终为窗口内的元素
            maxHeap.remove(num[i]);
            maxHeap.add(num[j]);
            ret.add(maxHeap.peek());
        }
        return ret;
    }
}

 

题目-矩阵中的路径

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如   矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。 

思路

1.这是一个典型的回溯法应用场景。

2.首先需要创建一个辅助数组 来 标记 每个位置是否是字符串中的字符;再创建一个变量,用来存储当前路径的长度。当其长度与字符串一样时,说明已经完成。

3.先遍历矩阵,找到和字符串第一个字符相同的字符,作为路径的开始;

4.函数中首先要进行合法性判断&当前路径的字符和字符串上对应位置的字符是否相等。符合条件才能进行下面的递归遍历;

5.矩阵路径此时+1,并且标记辅助数组中的该位置为已访问;

6.由于“每一步可以在矩阵中向左,向右,向上,向下移动一个格子”。接下来对该位置的上下左右四个位置进行递归遍历,在递归中,对于各位置找寻下一个位置。如果找到了,就能够一条顺序下去找到完整的路径,返回true;

7.如果没有找到,需要将路径-1,且标记该位置重新为未访问(清除之前的标记状态)。因为要重新寻找。

解法

public class Solution {
    private final static int[][] next = {{0,-1},{0,1},{-1,0},{1,0}};
    private int rows;
    private int cols;
     
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
    {
        if(matrix==null || rows<=0 || cols<=0 || str==null){
            return false;
        }
        if(str.length==0){
            return true;
        }
        //用来标记路径中已经访问过的结点
        boolean[] marked=new boolean[matrix.length];
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                //先遍历矩阵,找到和字符串第一个字符相同的字符,作为路径的开始
                if(backtracking(matrix, str, marked, 0, i, j, rows, cols))
                    return true;
            }
        }
        return false;
    }
    //尝试寻找路径
    public boolean backtracking(char[] matrix, char[] str, boolean[] marked, int k, int row, int col, int rows, int cols){
        //合法性判断&当前路径的字符和字符串上对应位置的字符是否相等
        if(row<0 || row>=rows || col<0 || col>=cols ||
                  str[k]!=matrix[row*cols+col] || marked[row*cols+col]){
            return false;
        }
        //直到路径字符串上所有字符都在矩阵中找到合适的位置
        if(k==str.length-1){
            return true;
        }
        //要搜索这个结点,首先需要将其标记为已经使用,防止重复使用
        marked[row*cols+col]=true;
        //对当前位置的上下左右4个相邻的格子进行遍历,匹配到下一个字符
        if(backtracking(matrix, str, marked, k+1, row+1, col, rows, cols) ||
          backtracking(matrix, str, marked, k+1, row, col+1, rows, cols) ||
          backtracking(matrix, str, marked, k+1, row-1, col, rows, cols) ||
          backtracking(matrix, str, marked, k+1, row, col-1, rows, cols)){
            return true;
        }
        //如果4个相邻的格子都没有匹配字符串中下一个的字符,表明当前路径字符串中字符在矩阵中的定位不正确,我们需要回到前一个,然后重新定位。
        //这一次搜索结束后,需要将该结点的已经使用状态清除
        marked[row*cols+col]=false;
        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,0的各自开始移动”,上一题需要我们去遍历数组,得到一个合适的路径开始点;

2.本题中有一些格子不能够走(行坐标和列坐标的数位之和大于k的格子)。每一步移动时,还需要添加一步来判断这个各自是否合法(再合法性判断时添加即可)。也就是相当于上一题中需要判断结点是否匹配的效果。

解法 

public class Solution {
    public int movingCount(int threshold, int rows, int cols)
    {
        if(rows <= 0 || cols <= 0){
            return 0;
        }
        //标记已经访问过的位置
        boolean[] marked = new boolean[rows*cols];
        for(int i=0; i<marked.length; i++){
            marked[i] = false;
        }
        //从(0,0)坐标开始移动
        int count = movingCountCore(threshold, rows, cols, 0, 0, marked);
        return count;
    }
     
    public int movingCountCore(int threshold, int rows, int cols, int row, int col, boolean[] marked){
        int count = 0;
        if(row>=0 && row<rows && col>=0 && col<cols &&
          getDigitSum(row)+getDigitSum(col)<=threshold &&
          !marked[row*cols+col]){
            marked[row*cols+col] = true;
            count = 1+movingCountCore(threshold, rows, cols, row-1, col, marked)+
                    movingCountCore(threshold, rows, cols, row+1, col, marked)+
                    movingCountCore(threshold, rows, cols, row, col-1, marked)+
                    movingCountCore(threshold, rows, cols, row, col+1, marked);
        }
        return count;
    }
     
    public int getDigitSum(int num){
        int sum = 0;
        while(num > 0){
            sum += (num%10);
            num /= 10;
        }
        return sum;
    }
     
     
}

 

题目-剪绳子

给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],...,k[m]。请问k[0]xk[1]x...xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。 

思路 

1.本题可以使用贪婪算法,但是并不是所有问题都可以通过 贪婪算法 得到最优解,依赖于数学证明。贪婪算法的效率也更高。(可以证明出当绳子大于5时,尽可能剪出长度为3的绳子是最优解。当所有绳子长度相等时,乘积最大)

2.我们使用动态规划(每一步都是重新将子问题的最优值相加计算出的)来解决这个问题。虽然很多子问题会重复求解,效率不高。

  要求的是乘积最大值,那么定义一个函数f(n)表示长度为n的绳子剪成若干段后 各段 乘积 的最大值。

  当n=1时,最大乘积只能为0=1*0;

  当n=2时,最大乘积只能为1=1*1;

  当n=3时,最大乘积只能为2=1*2;

  当n=4时,可以分为如下几种情况:1*1*1*1,1*2*1,1*3,2*2,最大乘积为4。

  剪第一刀时,选择下第一刀的地方有1~n-1这些地方,有n-1种可能。剪出来的第一段的绳子可能长度为1,2,3,...,n-1。假设在第i处下刀,绳子将分为[0,i]和[i,n],长度分别为i与n-i。那么找出第一刀最合适的位置,就是找i在哪里下刀,可以使得[0,i]与[i,n]的乘积最大。

  即f(n)=max( f(i)*f(n-i) ),0<i<n。

  从下往上推,先计算f(1)接着计算f(2)...直到得到f(n)。大的问题通过按照小问题的最优组合得到。

3.比如要求长度为10的绳子,就计算1-9这9种长度的绳子,每种长度的最大乘积是多少。要求长度为9的绳子,就计算1-8这8种长度的绳子,每种长度的最大乘积是多少。以此类推。

解法

//贪婪算法
public
class Solution { public int cutRope(int target) {
     if(n<=3)
return n-1; int a = n/3;
int b = n%3;
if(b==0){
return (int)Math.pow(3,a);
}
         else if(b==1){
              return (int)Math.pow(3,a-1)*4;
         }
         else{
              return (int)Math.pow(3,a)*2;
         }
} }

 

public int cutRope(int length) {
        if (length < 2) {
            return 0;
        }
        if (length == 2) {
            return 1;
        }
        if (length == 3) {
            return 2;
        }
        //存储长度从 0-len 的最大结果
        int[] products = new int[length + 1];  // 将最优解存储在数组中
        // 数组中第i个元素表示把长度为i的绳子剪成若干段之后的乘积的最大值
        products[0] = 0;
        products[1] = 1;
        products[2] = 2;
        products[3] = 3;for (int i = 4; i <= length; i++) {  //i表示长度
          
//不论i是奇数还是偶数,只考虑前i/2个剪切位置即可,后面的剪切位置重复
for (int j = 1; j <= i / 2; j++) { product[i]=Math.max(product[i], product[j]*product[i-j]); } }return product[length]; } }

 

题目-图的广度优先遍历

用队列记录下一步的走向。

1.访问下一个未访问的邻接点,这个顶点必须是当前顶点的邻接点,标记它,加入队列

2.如果因为已经没有 未访问顶点,而不能执行上一条规则,那么从队列头取一个顶点,使其成为当前顶点

3.直到队列为空,完成搜索

题解

public void searchTraversing(GraphNode node){
         ArrayList<GraphNode> visited=new ArrayList<GraphNode>();
         ArrayList<GraphNode> list=new ArrayList<GraphNode>();
         Queue<GraphNode> queue = new LinkedList<GraphNode>();
         queue.add(node);
         while(!queue.isEmpty()){
                 GraphNode curNode=queue.poll();
                  if(!visited.contain(curNode)){
                           visited.add(curNode);
                           list.add(curNode.getLabel());
                           for(int i=0;i<curNode.edgeList.size();i++){
                                    queue.offer(curNode.edgeList.get(i).getNodeRight());
                            }

                  }

          } 
 
}

 

(思路部分的图均源于网络,侵删)

 

posted @ 2020-03-05 18:26  闲不住的小李  阅读(194)  评论(0编辑  收藏  举报