Code Ganker: 四月 2014

2014年4月30日星期三

4Sum -- LeetCode

原题链接: http://oj.leetcode.com/problems/4sum/
这道题要求跟3Sum差不多,只是需求扩展到四个的数字的和了。我们还是可以按照3Sum中的解法,只是在外面套一层循环,相当于求n次3Sum。我们知道3Sum的时间复杂度是O(n^2),所以如果这样解的总时间复杂度是O(n^3)。代码如下:
public ArrayList<ArrayList<Integer>> fourSum(int[] num, int target) {
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    if(num==null||num.length==0)
        return res;
    Arrays.sort(num);
    for(int i=num.length-1;i>2;i--)
    {
        if(i==num.length-1 || num[i]!=num[i+1])
        {
            ArrayList<ArrayList<Integer>> curRes = threeSum(num,i-1,target-num[i]);
            for(int j=0;j<curRes.size();j++)
            {
                curRes.get(j).add(num[i]);
            }
            res.addAll(curRes);
        }
    }
    return res;        
}
private ArrayList<ArrayList<Integer>> threeSum(int[] num, int end, int target)
{
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    for(int i=end;i>1;i--)
    {
        if(i==end || num[i]!=num[i+1])
        {
            ArrayList<ArrayList<Integer>> curRes = twoSum(num,i-1,target-num[i]);
            for(int j=0;j<curRes.size();j++)
            {
                curRes.get(j).add(num[i]);
            }
            res.addAll(curRes);
        }
    }
    return res;
}
private ArrayList<ArrayList<Integer>> twoSum(int[] num, int end, int target)
{
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    int l=0;
    int r=end;
    while(l<r)
    {
        if(num[l]+num[r]==target)
        {
            ArrayList<Integer> item = new ArrayList<Integer>();
            item.add(num[l]);
            item.add(num[r]);
            res.add(item);
            l++;
            r--;
            while(l<r&&num[l]==num[l-1])
                l++;
            while(l<r&&num[r]==num[r+1])
                r--;
        }
        else if(num[l]+num[r]>target)
        {
            r--;
        }
        else
        {
            l++;
        }
    }
    return res;
}
上述这种方法比较直接,根据3Sum的结果很容易进行推广。那么时间复杂度能不能更好呢?其实我们可以考虑用二分法的思路,如果把所有的两两pair都求出来,然后再进行一次Two Sum的匹配,我们知道Two Sum是一个排序加上一个线性的操作,并且把所有pair的数量是O((n-1)+(n-2)+...+1)=O(n(n-1)/2)=O(n^2)。所以对O(n^2)的排序如果不用特殊线性排序算法是O(n^2*log(n^2))=O(n^2*2logn)=O(n^2*logn),算法复杂度比上一个方法的O(n^3)是有提高的。
思路虽然明确,不过细节上会多很多情况要处理。首先,我们要对每一个pair建一个数据结构来存储元素的值和对应的index,这样做是为了后面当找到合适的两对pair相加能得到target值时看看他们是否有重叠的index,如果有说明它们不是合法的一个结果,因为不是四个不同的元素。接下来我们还得对这些pair进行排序,所以要给pair定义comparable的函数。最后,当进行Two Sum的匹配时因为pair不再是一个值,所以不能像Two Sum中那样直接跳过相同的,每一组都得进行查看,这样就会出现重复的情况,所以我们还得给每一个四个元素组成的tuple定义hashcode和相等函数,以便可以把当前求得的结果放在一个HashSet里面,这样得到新结果如果是重复的就不加入结果集了。
代码如下:
public class Node
{
    int index;
    int value;
    public Node(int index, int value)
    {
        this.index = index;
        this.value = value;
    }
}
public class Pair
{
    Node[] nodes;
    public Pair(Node n1, Node n2)
    {
        nodes = new Node[2];
        nodes[0] = n1;
        nodes[1] = n2;
    }
    public int getSum()
    {
        return nodes[0].value+nodes[1].value;
    }
}
public class Tuple
{
    ArrayList<Integer> num;
    public Tuple(ArrayList<Integer> num)
    {
        this.num = new ArrayList<Integer>(num);
        Collections.sort(this.num);
    }
    public int hashCode() {
        int hashcode = 5;
        for(int i=0;i<this.num.size();i++)
        {
            hashcode = 31*hashcode + this.num.get(i);
        }
        return hashcode;
    }
    public boolean equals(Object obj) {
        if (obj == null)
            return false;
        if (obj == this)
            return true;
        if (!(obj instanceof Tuple))
            return false;
        Tuple rhs = (Tuple) obj;
        for(int i=0;i<this.num.size();i++)
        {
            if(!this.num.get(i).equals(rhs.num.get(i)))
            {
                return false;
            }
        }            
        return true;
    }
}
public ArrayList<ArrayList<Integer>> fourSum(int[] num, int target) {
    ArrayList<Pair> pairs = getPairs(num);
    Comparator<Pair> pairComparator = new Comparator<Pair>(){
        @Override
        public int compare(Pair p1, Pair p2){
            return p1.getSum()-p2.getSum();
        }
    };
    Collections.sort(pairs, pairComparator);
    return twoSum(pairs, target);
}
private ArrayList<Pair> getPairs(int[] num)
{
    ArrayList<Pair> res = new ArrayList<Pair>();
    for(int i=0;i<num.length-1;i++)
    {
        Node n1 = new Node(i,num[i]);
        for(int j=i+1;j<num.length;j++)
        {
            Node n2 = new Node(j,num[j]);
            Pair pair = new Pair(n1,n2);
            res.add(pair);
        }
    }
    return res;
}
private ArrayList<ArrayList<Integer>> twoSum(ArrayList<Pair> pairs, int target){
    HashSet<Tuple> hashSet = new HashSet<Tuple>();
    int l = 0;
    int r = pairs.size()-1;
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    while(l<r){
        if(pairs.get(l).getSum()+pairs.get(r).getSum()==target)
        {
            int lEnd = l;
            int rEnd = r;
            while(lEnd<rEnd && pairs.get(lEnd).getSum()==pairs.get(lEnd+1).getSum())
            {
                lEnd++;
            }
            while(lEnd<rEnd && pairs.get(rEnd).getSum()==pairs.get(rEnd-1).getSum())
            {
                rEnd--;
            }
            for(int i=l;i<=lEnd;i++)
            {
                for(int j=r;j>=rEnd;j--)
                {
                    if(check(pairs.get(i),pairs.get(j)))
                    {
                        ArrayList<Integer> item = new ArrayList<Integer>();
                        item.add(pairs.get(i).nodes[0].value);
                        item.add(pairs.get(i).nodes[1].value);
                        item.add(pairs.get(j).nodes[0].value);
                        item.add(pairs.get(j).nodes[1].value);
                        //Collections.sort(item);
                        Tuple tuple = new Tuple(item);
                        if(!hashSet.contains(tuple))
                        {
                            hashSet.add(tuple);
                            res.add(tuple.num);
                        }
                    }                        
                }
            }
            l = lEnd+1;
            r = rEnd-1;
        }
        else if(pairs.get(l).getSum()+pairs.get(r).getSum()>target)
        {
            r--;
        }
        else{
            l++;
        }
    }
    return res;
}
private boolean check(Pair p1, Pair p2)
{
    if(p1.nodes[0].index == p2.nodes[0].index || p1.nodes[0].index == p2.nodes[1].index)
        return false;
    if(p1.nodes[1].index == p2.nodes[0].index || p1.nodes[1].index == p2.nodes[1].index)
        return false;
    return true;
}
第二种方法比第一种方法时间上还是有提高的,其实这道题可以推广到k-Sum的问题,基本思想就是和第二种方法一样进行二分,然后两两结合,不过细节就非常复杂了(这点从上面的第二种解法就能看出来),所以不是很适合在面试中出现,有兴趣的朋友可以进一步思考或者搜索网上材料哈。

2014年4月29日星期二

Unique Binary Search Trees II -- LeetCode

原题链接: http://oj.leetcode.com/problems/unique-binary-search-trees-ii/
这道题是求解所有可行的二叉查找树,从Unique Binary Search Trees中我们已经知道,可行的二叉查找树的数量是相应的卡特兰数,不是一个多项式时间的数量级,所以我们要求解所有的树,自然是不能多项式时间内完成的了。算法上还是用求解NP问题的方法来求解,也就是N-Queens中介绍的在循环中调用递归函数求解子问题。思路是每次一次选取一个结点为根,然后递归求解左右子树的所有结果,最后根据左右子树的返回的所有子树,依次选取然后接上(每个左边的子树跟所有右边的子树匹配,而每个右边的子树也要跟所有的左边子树匹配,总共有左右子树数量的乘积种情况),构造好之后作为当前树的结果返回。代码如下:
public ArrayList<TreeNode> generateTrees(int n) {
    return helper(1,n);
}
private ArrayList<TreeNode> helper(int left, int right)
{
    ArrayList<TreeNode> res = new ArrayList<TreeNode>();
    if(left>right)
    {
        res.add(null);
        return res;
    }
    for(int i=left;i<=right;i++)
    {
        ArrayList<TreeNode> leftList = helper(left,i-1);
        ArrayList<TreeNode> rightList = helper(i+1,right);
        for(int j=0;j<leftList.size();j++)
        {
            for(int k=0;k<rightList.size();k++)
            {
                TreeNode root = new TreeNode(i);
                root.left = leftList.get(j);
                root.right = rightList.get(k);
                res.add(root);
            }
        }
    }
    return res;
}
实现中还是有一些细节的,因为构造树时两边要遍历所有左右的匹配,然后接到根上面。
当然我们也可以像在Unique Binary Search Trees中那样存储所有的子树历史信息,然后进行拼合,虽然可以省一些时间,但是最终还是逃不过每个结果要一次运算,时间复杂度还是非多项式的,并且要耗费大量的空间,感觉这样的意义就不是很大了。

Unique Binary Search Trees -- LeetCode

原题链接: http://oj.leetcode.com/problems/unique-binary-search-trees/
这道题要求可行的二叉查找树的数量,其实二叉查找树可以任意取根,只要满足中序遍历有序的要求就可以。从处理子问题的角度来看,选取一个结点为根,就把结点切成左右子树,以这个结点为根的可行二叉树数量就是左右子树可行二叉树数量的乘积,所以总的数量是将以所有结点为根的可行结果累加起来。写成表达式如下:
熟悉卡特兰数的朋友可能已经发现了,这正是卡特兰数的一种定义方式,是一个典型的动态规划的定义方式(根据其实条件和递推式求解结果)。所以思路也很明确了,维护量res[i]表示含有i个结点的二叉查找树的数量。根据上述递推式依次求出1到n的的结果即可。
时间上每次求解i个结点的二叉查找树数量的需要一个i步的循环,总体要求n次,所以总时间复杂度是O(1+2+...+n)=O(n^2)。空间上需要一个数组来维护,并且需要前i个的所有信息,所以是O(n)。代码如下:
public int numTrees(int n) {
    if(n<=0)
        return 0;
    int[] res = new int[n+1];
    res[0] = 1;
    res[1] = 1;
    for(int i=2;i<=n;i++)
    {
        for(int j=0;j<i;j++)
        {
            res[i] += res[j]*res[i-j-1];
        }
    }
    return res[n];
}
这种求数量的题目一般都容易想到用动态规划的解法,这道题的模型正好是卡特兰数的定义。当然这道题还可以用卡特兰数的通项公式来求解,这样时间复杂度就可以降低到O(n)。因为比较直接,这里就不列举代码了。
如果是求解所有满足要求的二叉树(而不仅仅是数量)那么时间复杂度是就取决于结果的数量了,不再是一个多项式的解法了,有兴趣的朋友可以看看Unique Binary Search Trees II

2014年4月28日星期一

Restore IP Addresses -- LeetCode

原题链接: http://oj.leetcode.com/problems/restore-ip-addresses/
这道题的解法非常接近于NP问题,也是采用递归的解法。基本思路就是取出一个合法的数字,作为IP地址的一项,然后递归处理剩下的项。可以想象出一颗树,每个结点有三个可能的分支(因为范围是0-255,所以可以由一位两位或者三位组成)。并且这里树的层数不会超过四层,因为IP地址由四段组成,到了之后我们就没必要再递归下去,可以结束了。这里除了上述的结束条件外,另一个就是字符串读完了。可以看出这棵树的规模是固定的,不会像平常的NP问题那样,时间复杂度取决于输入的规模,是指数量级的,所以这道题并不是NP问题,因为他的分支是四段,有限制。代码如下:
public ArrayList<String> restoreIpAddresses(String s) {
    ArrayList<String> res = new ArrayList<String>();
    if(s==null || s.length()==0)
        return res;
    helper(s,0,1,"",res);
    return res;
}
private void helper(String s, int index, int segment, String item, ArrayList<String> res)
{
    if(index>=s.length())
        return;
    if(segment == 4)
    {
        String str = s.substring(index);
        if(isValid(str))
        {
            res.add(item+"."+str);
        }
        return;
    }
    for(int i=1;i<4&&(i+index<=s.length());i++)
    {
        String str = s.substring(index, index+i);
        if(isValid(str))
        {
            if(segment==1)
                helper(s,index+i,segment+1,str,res);
            else
                helper(s,index+i,segment+1,item+"."+str,res);
        }
    }
}
private boolean isValid(String str)
{
    if(str==null || str.length()>3)
        return false;
    int num = Integer.parseInt(str);
    if(str.charAt(0)=='0' && str.length()>1)
        return false;
    if(num>=0 && num<=255)
        return true;
    return false;
}
实现中需要一个判断数字是否为合法ip地址的一项的函数,首先要在0-255之间,其次前面字符不能是0。剩下的就是NP问题的套路了,递归中套一个for循环,不熟悉的朋友可以看看N-Queens哈。

Interleaving String -- LeetCode

原题链接: http://oj.leetcode.com/problems/interleaving-string/
这是一道关于字符串操作的题目,要求是判断一个字符串能不能由两个字符串按照他们自己的顺序,每次挑取两个串中的一个字符来构造出来。
像这种判断能否按照某种规则来完成求是否或者某个量的题目,很容易会想到用动态规划来实现。
先说说维护量,res[i][j]表示用s1的前i个字符和s2的前j个字符能不能按照规则表示出s3的前i+j个字符,如此最后结果就是res[s1.length()][s2.length()],判断是否为真即可。接下来就是递推式了,假设知道res[i][j]之前的所有历史信息,我们怎么得到res[i][j]。可以看出,其实只有两种方式来递推,一种是选取s1的字符作为s3新加进来的字符,另一种是选s2的字符作为新进字符。而要看看能不能选取,就是判断s1(s2)的第i(j)个字符是否与s3的i+j个字符相等。如果可以选取并且对应的res[i-1][j](res[i][j-1])也为真,就说明s3的i+j个字符可以被表示。这两种情况只要有一种成立,就说明res[i][j]为真,是一个或的关系。所以递推式可以表示成
res[i][j] = res[i-1][j]&&s1.charAt(i-1)==s3.charAt(i+j-1) || res[i][j-1]&&s2.charAt(j-1)==s3.charAt(i+j-1)
时间上因为是一个二维动态规划,所以复杂度是O(m*n),m和n分别是s1和s2的长度。最后就是空间花费,可以看出递推式中只需要用到上一行的信息,所以我们只需要一个一维数组就可以完成历史信息的维护,为了更加优化,我们把短的字符串放在内层循环,这样就可以只需要短字符串的长度即可,所以复杂度是O(min(m,n))。代码如下:
public boolean isInterleave(String s1, String s2, String s3) {
    if(s1.length()+s2.length()!=s3.length())
        return false;
    String minWord = s1.length()>s2.length()?s2:s1;
    String maxWord = s1.length()>s2.length()?s1:s2;
    boolean[] res = new boolean[minWord.length()+1];
    res[0] = true;
    for(int i=0;i<minWord.length();i++)
    {
        res[i+1] = res[i] && minWord.charAt(i)==s3.charAt(i);
    }
    for(int i=0;i<maxWord.length();i++)
    {
        res[0] = res[0] && maxWord.charAt(i)==s3.charAt(i);
        for(int j=0;j<minWord.length();j++)
        {
            res[j+1] = res[j+1]&&maxWord.charAt(i)==s3.charAt(i+j+1) || res[j]&&minWord.charAt(j)==s3.charAt(i+j+1);
        }
    }
    return res[minWord.length()];
}
动态规划其实还是有套路的,无非就是找到维护量,然后得到递推式,接下来看看历史信息对于空间的需求,尽量优化,会在后面对于动态规划做一个比较通用的总结哈。

2014年4月27日星期日

Reverse Linked List II -- LeetCode

原题链接: http://oj.leetcode.com/problems/reverse-linked-list-ii/
这道题是比较常见的链表反转操作,不过不是反转整个链表,而是从m到n的一部分。分为两个步骤,第一步是找到m结点所在位置,第二步就是进行反转直到n结点。反转的方法就是每读到一个结点,把它插入到m结点前面位置,然后m结点接到读到结点的下一个。总共只需要一次扫描,所以时间是O(n),只需要几个辅助指针,空间是O(1)。代码如下:
public ListNode reverseBetween(ListNode head, int m, int n) {
    if(head == null)
        return null;
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode preNode = dummy;
    int i=1;
    while(preNode.next!=null && i<m)
    {
        preNode = preNode.next;
        i++;
    }
    if(i<m)
        return head;
    ListNode mNode = preNode.next;
    ListNode cur = mNode.next;
    while(cur!=null && i<n)
    {
        ListNode next = cur.next;
        cur.next = preNode.next;
        preNode.next = cur;
        mNode.next = next;
        cur = next;
        i++;
    }
    return dummy.next;
}
上面的代码还是有些细节的,链表的题目就是这样,想起来道理很简单,实现中可能会出些小差错,还是熟能生巧哈。

Subsets II -- LeetCode

原题链接: http://oj.leetcode.com/problems/subsets-ii/
这道题跟Subsets一样是经典的NP问题--求子集。比Subsets稍微复杂一些的是这里的集合中可能出现重复元素,因此我们在求子集的时候要避免出现重复的子集。在Subsets中我们每次加进一个元素就会把原来的子集都加上这个元素,然后再加入到结果集中,但是这样重复的元素就会产生重复的子集。为了避免这样的重复,需要用个小技巧。
其实比较简单,就是每当遇到重复元素的时候我们就只把当前结果集的后半部分加上当前元素加入到结果集中,因为后半部分就是上一步中加入这个元素的所有子集,上一步这个元素已经加入过了,前半部分如果再加就会出现重复。所以算法上复杂度上没有提高,反而少了一些操作,就是遇到重复时少做一半,不过这里要对元素集合先排序,否则不好判断重复元素。同样的还是可以用递归和非递归来解,不过对于重复元素的处理是一样的。递归的代码如下:
public ArrayList<ArrayList<Integer>> subsetsWithDup(int[] num) {
    if(num == null)
        return null;
    Arrays.sort(num);
    ArrayList<Integer> lastSize = new ArrayList<Integer>();
    lastSize.add(0);
    return helper(num, num.length-1, lastSize);
}
private ArrayList<ArrayList<Integer>> helper(int[] num, int index, ArrayList<Integer> lastSize)
{
    if(index == -1)
    {
        ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
        ArrayList<Integer> elem = new ArrayList<Integer>();
        res.add(elem);
        return res;
    }
    ArrayList<ArrayList<Integer>> res = helper(num,index-1,lastSize);
    int size = res.size();
    int start = 0;
    if(index>0 && num[index]==num[index-1])
        start = lastSize.get(0);
    for(int i=start;i<size;i++)
    {
        ArrayList<Integer> elem = new ArrayList<Integer>(res.get(i));
        elem.add(num[index]);
        res.add(elem);
    }
    lastSize.set(0,size);
    return res;
}
非递归的代码如下:
public ArrayList<ArrayList<Integer>> subsetsWithDup(int[] num) {
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    res.add(new ArrayList<Integer>());
    if(num==null || num.length==0)
        return res;
    Arrays.sort(num);
    int start = 0;
    for(int i=0;i<num.length;i++)
    {
        int size = res.size();
        for(int j=start;j<size;j++)
        {
            ArrayList<Integer> newItem = new ArrayList<Integer>(res.get(j));
            newItem.add(num[i]);
            res.add(newItem);
        }
        if(i<num.length-1 && num[i]==num[i+1])
        {
            start = size;
        }
        else
        {
            start = 0;
        }
    }
    return res;
}
这种NP问题的重复处理在LeetCode有一定出现频率,比如还有Permutations II也是这样的,其实本质就是当一个重复元素进来时忽略上一个元素已经有的结果,只考虑由重复元素所产生的新结果。

2014年4月26日星期六

Decode Ways -- LeetCode

原题链接: http://oj.leetcode.com/problems/decode-ways/
这道题要求解一个数字串按照字符串编码方式可解析方式的数量。看到这种求数量的,我们很容易想到动态规划来存储前面信息,然后迭代得到最后结果。
我们维护的量res[i]是表示前i个数字有多少种解析的方式,接下来来想想递归式,有两种方式:第一种新加进来的数字不然就是自己比较表示一个字符,那么解析的方式有res[i-1]种,第二种就是新加进来的数字和前一个数字凑成一个字符,解析的方式有res[i-2]种(因为上一个字符和自己凑成了一个)。当然这里要判断前面说的两种情况能否凑成一个字符,也就是范围的判断,如果可以才有对应的解析方式,如果不行,那么就是0。最终结果就是把这两种情况对应的解析方式相加。这里可以把范围分成几个区间:
(1)00:res[i]=0(无法解析,没有可行解析方式);
(2)10, 20:res[i]=res[i-2](只有第二种情况成立);
(3)11-19, 21-26:res[i]=res[i-1]+res[i-2](两种情况都可行);
(4)01-09, 27-99:res[i]=res[i-1](只有第一种情况可行);
递推式就是按照上面的规则来得到,接下来我们只要进行一遍扫描,然后依次得到维护量就可以了,算法的时间复杂度是O(n)。空间上可以看出我们每次只需要前两位的历史信息,所以只需要维护三个变量然后迭代赋值就可以了,所以空间复杂度是O(1)。代码如下:
public int numDecodings(String s) {
    if(s==null || s.length()==0 || s.charAt(0)=='0')
    {
        return 0;
    }
    int num1=1;
    int num2=1;
    int num3=1;
    for(int i=1;i<s.length();i++)
    {
        if(s.charAt(i)=='0')
        {
            if(s.charAt(i-1)=='1' || s.charAt(i-1)=='2')
                num3 = num1;
            else
                return 0;
        }
        else
        {
            if(s.charAt(i-1)=='0' || s.charAt(i-1)>='3')
                num3 = num2;
            else
            {
                if(s.charAt(i-1)=='2' && s.charAt(i)>='7' && s.charAt(i)<='9')
                    num3 = num2;
                else
                    num3 = num1+num2;
            }
        }
        num1 = num2;
        num2 = num3;
    }
    return num2;
}
这道题是一维动态规划的题目,递推式关系来说是比较容易得到的,主要是要对这些两位数进行划分有一些细节,容易出小错误。

Recover Binary Search Tree -- LeetCode

原题链接: http://oj.leetcode.com/problems/recover-binary-search-tree/
这道题是要求恢复一颗有两个元素调换错了的二叉查找树。一开始拿到可能会觉得比较复杂,其实观察出规律了就比较简单。主要还是利用二叉查找树的主要性质,就是中序遍历是有序的性质。那么如果其中有元素被调换了,意味着中序遍历中必然出现违背有序的情况。那么会出现几次呢?有两种情况,如果是中序遍历相邻的两个元素被调换了,很容易想到就只需会出现一次违反情况,只需要把这个两个节点记录下来最后调换值就可以;如果是不相邻的两个元素被调换了,举个例子很容易可以发现,会发生两次逆序的情况,那么这时候需要调换的元素应该是第一次逆序前面的元素,和第二次逆序后面的元素。比如1234567,1和5调换了,会得到5234167,逆序发生在52和41,我们需要把4和1调过来,那么就是52的第一个元素,41的第二个元素调换即可。
搞清楚了规律就容易实现了,中序遍历寻找逆序情况,调换的第一个元素,永远是第一个逆序的第一个元素,而调换的第二个元素如果只有一次逆序,则是那一次的后一个,如果有两次逆序则是第二次的后一个。算法只需要一次中序遍历,所以时间复杂度是O(n),空间是栈大小O(logn)。代码如下:
public void recoverTree(TreeNode root) {
    if(root == null)
        return;
    ArrayList<TreeNode> pre = new ArrayList<TreeNode>();
    pre.add(null);
    ArrayList<TreeNode> res = new ArrayList<TreeNode>();
    helper(root,pre, res);
    if(res.size()>0)
    {
        int temp = res.get(0).val;
        res.get(0).val = res.get(1).val;
        res.get(1).val = temp;
    }
}
private void helper(TreeNode root, ArrayList<TreeNode> pre, ArrayList<TreeNode> res)
{
    if(root == null)
    {
        return;
    }
    helper(root.left, pre, res);
    if(pre.get(0)!=null && pre.get(0).val>root.val)
    {
        if(res.size()==0)
        {
            res.add(pre.get(0));
            res.add(root);
        }
        else
        {
            res.set(1,root);
        }
    }
    pre.set(0,root);
    helper(root.right,pre,res);
}
可以看到实现中pre用了一个ArrayList来存,这样做的原因是因为java都是值传递,所以我们要全局变化pre的值(而不是在当前函数里),只能传一个数组,才能改变结点的地址,这一点非常重要,也是java和C++一个比较大的区别,不了解的朋友可以研究一下哈。
这道题还是考察二叉树遍历,不过应用题目要求套了一个不同的外壳,需要我们利用二叉查找树的性质观察出规律之后才能求解。

2014年4月25日星期五

Gray Code -- LeetCode

原题链接: http://oj.leetcode.com/problems/gray-code/
这道题要求求出n位的格雷码对应的二进制数,主要在于找到一种格雷码的递增方法(gray code并不是唯一的,可以有多种)。
我们来看看有了n-1位的格雷码如何得到n位的格雷码呢?其实方法比较简单,首先在n-1位的格雷码前面都添加0作为前2^(n-1)个格雷码,它们一定是合法的因为除了第一位(都是0)其余位都跟n-1的格雷码一致,所以两两之间只相差一位,满足要求。接下来看看如何接上剩下的2^(n-1)个,我们把n-1位的格雷码倒序排列,然后在每个前面添加1,然后接到上述的前2^(n-1)个就可以了。首先因为是倒序过来,所以中间两个元素除去第一位其他都是一样的(因为原来最后一个,现在倒序过来就是第一个),而他们第一位分别是0和1,满足格雷码的要求。而剩下的元素因为我们是n-1位的格雷码倒序排列,所以两两都是满足要求的,加上前面都一样的位1,仍然满足条件。最后看看这些数字是不是都不一样,前半部分和后半部分肯定不会一样,而因为前半部分都是0开头,后半部分都是1打头,所以互相之间也不会有重复,可以看出覆盖了所有数字,而且依次下来均满足条件。
如此我们提出了格雷码的递推方法,我们只需要做一次位数的循环,每次根据上面结果构造当前位数的结果即可。算法复杂度是O(2+2^2+...+2^n-1)=O(2^n),所以是指数量级的,因为是结果数量无法避免。空间复杂度则是结果的大小,也是O(2^n)。代码如下:
public ArrayList<Integer> grayCode(int n) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(n<0)
        return res;
    if(n==0)
    {
        res.add(0);
        return res;
    }
    res.add(0);
    res.add(1);
    for(int i=2;i<=n;i++)
    {
        int size = res.size();
        for(int j=size-1;j>=0;j--)
        {
            res.add(res.get(j)+(1<<(i-1)));
        }
    }
    return res;
}
可以看出上面代码并不需要处理前半部分,因为要求的是二进制数对应的整数,所以在前面加0等于原来的数字,所以前面数字只需要保持原来就行,后面进行倒序然后对最高位赋1即可。感觉更接近于一道寻找规律的题目,实现上用到一点位运算会比较简单,不过位运算是这道题的考察点之一,面试中还是有关于位运算的题目,需要熟悉一下哈。

Binary Tree Zigzag Level Order Traversal -- LeetCode

原题链接: http://oj.leetcode.com/problems/binary-tree-zigzag-level-order-traversal/
这道题其实还是树的层序遍历Binary Tree Level Order Traversal,如果不熟悉的朋友可以先看看哈。不过这里稍微做了一点变体,就是在遍历的时候偶数层自左向右,而奇数层自右向左。在Binary Tree Level Order Traversal中我们是维护了一个队列来完成遍历,而在这里为了使每次都倒序出来,我们很容易想到用栈的结构来完成这个操作。有一个区别是这里我们需要一层一层的来处理(原来可以按队列插入就可以,因为后进来的元素不会先处理),所以会同时维护新旧两个栈,一个来读取,一个存储下一层结点。总体来说还是一次遍历完成,所以时间复杂度是O(n),空间复杂度最坏是两层的结点,所以数量级还是O(n)(满二叉树最后一层的结点是n/2个)。代码如下:
public ArrayList<ArrayList<Integer>> zigzagLevelOrder(TreeNode root) {
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    if(root==null)
        return res;
    LinkedList<TreeNode> stack = new LinkedList<TreeNode>();
    int level=1;
    ArrayList<Integer> item = new ArrayList<Integer>();
    item.add(root.val);
    res.add(item);
    stack.push(root);
    while(!stack.isEmpty())
    {
        LinkedList<TreeNode> newStack = new LinkedList<TreeNode>();
        item = new ArrayList<Integer>();
        while(!stack.isEmpty())
        {
            TreeNode node = stack.pop();
            if(level%2==0)
            {
                if(node.left!=null)
                {
                    newStack.push(node.left);
                    item.add(node.left.val);
                }
                if(node.right!=null)
                {
                    newStack.push(node.right);
                    item.add(node.right.val);
                }
            }
            else
            {
                if(node.right!=null)
                {
                    newStack.push(node.right);
                    item.add(node.right.val);
                }
                if(node.left!=null)
                {
                    newStack.push(node.left);
                    item.add(node.left.val);
                }                   
            }
        }
        level++;
        if(item.size()>0)
            res.add(item);
        stack = newStack;
    }
    return res;
}
上面的算法其实还是一次广度优先搜索,只是在读取每一层结点交替的交换顺序。毕竟面试中像Binary Tree Level Order Traversal有时候考得太多了,面试官会觉得你肯定练过,所以会稍作变体,来考察对于编程和算法的基本理解。

Scramble String -- LeetCode

原题链接: http://oj.leetcode.com/problems/scramble-string/
这道题看起来是比较复杂的,如果用brute force,每次做切割,然后递归求解,是一个非多项式的复杂度,一般来说这不是面试官想要的答案。
这其实是一道三维动态规划的题目,我们提出维护量res[i][j][n],其中i是s1的起始字符,j是s2的起始字符,而n是当前的字符串长度,res[i][j][len]表示的是以i和j分别为s1和s2起点的长度为len的字符串是不是互为scramble。
有了维护量我们接下来看看递推式,也就是怎么根据历史信息来得到res[i][j][len]。判断这个是不是满足,其实我们首先是把当前s1[i...i+len-1]字符串劈一刀分成两部分,然后分两种情况:第一种是左边和s2[j...j+len-1]左边部分是不是scramble,以及右边和s2[j...j+len-1]右边部分是不是scramble;第二种情况是左边和s2[j...j+len-1]右边部分是不是scramble,以及右边和s2[j...j+len-1]左边部分是不是scramble。如果以上两种情况有一种成立,说明s1[i...i+len-1]和s2[j...j+len-1]是scramble的。而对于判断这些左右部分是不是scramble我们是有历史信息的,因为长度小于n的所有情况我们都在前面求解过了(也就是长度是最外层循环)。
上面说的是劈一刀的情况,对于s1[i...i+len-1]我们有len-1种劈法,在这些劈法中只要有一种成立,那么两个串就是scramble的。
总结起来递推式是res[i][j][len] = || (res[i][j][k]&&res[i+k][j+k][len-k] || res[i][j+len-k][k]&&res[i+k][j][len-k]) 对于所有1<=k<len,也就是对于所有len-1种劈法的结果求或运算。因为信息都是计算过的,对于每种劈法只需要常量操作即可完成,因此求解递推式是需要O(len)(因为len-1种劈法)。
如此总时间复杂度因为是三维动态规划,需要三层循环,加上每一步需要线行时间求解递推式,所以是O(n^4)。虽然已经比较高了,但是至少不是指数量级的,动态规划还是有很大有事的,空间复杂度是O(n^3)。代码如下:
public boolean isScramble(String s1, String s2) {
    if(s1==null || s2==null || s1.length()!=s2.length())
        return false;
    if(s1.length()==0)
        return true;
    boolean[][][] res = new boolean[s1.length()][s2.length()][s1.length()+1];
    for(int i=0;i<s1.length();i++)
    {
        for(int j=0;j<s2.length();j++)
        {
            res[i][j][1] = s1.charAt(i)==s2.charAt(j);
        }
    }
    for(int len=2;len<=s1.length();len++)
    {
        for(int i=0;i<s1.length()-len+1;i++)
        {
            for(int j=0;j<s2.length()-len+1;j++)
            {
                for(int k=1;k<len;k++)
                {
                    res[i][j][len] |= res[i][j][k]&&res[i+k][j+k][len-k] || res[i][j+len-k][k]&&res[i+k][j][len-k];
                }
            }
        }
    }
    return res[0][0][s1.length()];
}
个人觉得这是LeetCode中最难的动态规划的题目了,要进行一次三维动态规划,对于维护量的含义也比较讲究。有朋友会讨论这个维护量是怎么提出来的,我自己也没什么绝对的方法,还是熟能生巧,靠“感觉”,做的题目多了就自然来了,这个做高中数学题有点类似哈,辅助线是靠“灵感”的哈。面试中如果遇到就是top难度的了,不过即使如此,只要思路清晰,还是可以记住的。如果没做过,个人觉得比较难当场想出来,不过算法大牛就另说了,这种题很经常出现在编程比赛中,ACM高手还是不在话下的哈。

2014年4月24日星期四

Partition List -- LeetCode

原题链接: http://oj.leetcode.com/problems/partition-list/
这是一道链表操作的题目,要求把小于x的元素按顺序放到链表前面。我们仍然是使用链表最常用的双指针大法,一个指向当前小于x的最后一个元素,一个进行往前扫描。如果元素大于x,那么继续前进,否则,要把元素移到前面,并更新第一个指针。这里有一个小细节,就是如果不需要移动(也就是已经是接在小于x的最后元素的后面了),那么只需要继续前进即可。算法时间复杂度是O(n),空间只需要几个辅助变量,是O(1)。代码如下:
public ListNode partition(ListNode head, int x) {
    if(head == null)
        return null;
    ListNode helper = new ListNode(0);
    helper.next = head;
    ListNode walker = helper;
    ListNode runner = helper;
    while(runner.next!=null)
    {
        if(runner.next.val<x)
        {
            if(walker!=runner)
            {
                ListNode next = runner.next.next;
                runner.next.next = walker.next;
                walker.next = runner.next;
                runner.next = next;
            }
            else
                runner = runner.next;
            walker = walker.next;
        }
        else
        {
            runner = runner.next;
        }
    }
    return helper.next;
}
这道题思路比较清晰,不过还是有点细节的,第一次写可能不容易完全写对,可以练习练习。

Maximal Rectangle -- LeetCode

原题链接: http://oj.leetcode.com/problems/maximal-rectangle/
这是一道非常综合的题目,要求在0-1矩阵中找出面积最大的全1矩阵。刚看到这道题会比较无从下手,brute force就是对于每个矩阵都看一下,总共有m(m+1)/2*n(n+1)/2个子矩阵(原理跟字符串子串类似,字符串的子串数有n(n+1)/2,只是这里是二维情形,所以是两个相乘),复杂度相当高,肯定不是面试官想要的答案,就不继续想下去了。
这道题的解法灵感来自于Largest Rectangle in Histogram这道题,假设我们把矩阵沿着某一行切下来,然后把切的行作为底面,将自底面往上的矩阵看成一个直方图(histogram)。直方图的中每个项的高度就是从底面行开始往上1的数量。根据Largest Rectangle in Histogram我们就可以求出当前行作为矩阵下边缘的一个最大矩阵。接下来如果对每一行都做一次Largest Rectangle in Histogram,从其中选出最大的矩阵,那么它就是整个矩阵中面积最大的子矩阵。
算法的基本思路已经出来了,剩下的就是一些节省时间空间的问题了。
我们如何计算某一行为底面时直方图的高度呢? 如果重新计算,那么每次需要的计算数量就是当前行数乘以列数。然而在这里我们会发现一些动态规划的踪迹,如果我们知道上一行直方图的高度,我们只需要看新加进来的行(底面)上对应的列元素是不是0,如果是,则高度是0,否则则是上一行直方图的高度加1。利用历史信息,我们就可以在线行时间内完成对高度的更新。我们知道,Largest Rectangle in Histogram的算法复杂度是O(n)。所以完成对一行为底边的矩阵求解复杂度是O(n+n)=O(n)。接下来对每一行都做一次,那么算法总时间复杂度是O(m*n)。
空间上,我们只需要保存上一行直方图的高度O(n),加上Largest Rectangle in Histogram中所使用的空间O(n),所以总空间复杂度还是O(n)。代码如下:
public int maximalRectangle(char[][] matrix) {
    if(matrix==null || matrix.length==0 || matrix[0].length==0)
    {
        return 0;
    }
    int maxArea = 0;
    int[] height = new int[matrix[0].length];
    for(int i=0;i<matrix.length;i++)
    {
        for(int j=0;j<matrix[0].length;j++)
        {
            height[j] = matrix[i][j]=='0'?0:height[j]+1;
        }
        maxArea = Math.max(largestRectangleArea(height),maxArea);
    }
    return maxArea;
}
public int largestRectangleArea(int[] height) {
    if(height==null || height.length==0)
    {
        return 0;
    }
    int maxArea = 0;
    LinkedList<Integer> stack = new LinkedList<Integer>();
    for(int i=0;i<height.length;i++)
    {
        
        while(!stack.isEmpty() && height[i]<=height[stack.peek()])
        {
            int index = stack.pop();
            int curArea = stack.isEmpty()?i*height[index]:(i-stack.peek()-1)*height[index];
            maxArea = Math.max(maxArea,curArea);
        }
        stack.push(i);
    }
    while(!stack.isEmpty())
    {
        int index = stack.pop();
        int curArea = stack.isEmpty()?height.length*height[index]:(height.length-stack.peek()-1)*height[index];
        maxArea = Math.max(maxArea,curArea);            
    }
    return maxArea;
}
这道题最后的复杂度是非常令人满意的,居然在O(m*n)时间内就可以完成对最大矩阵的搜索,可以看出这已经是下界(因为每个元素总要访问一下才知道是不是1)了。难度还是比较大的,相信要在面试当场想到这种方法是很不容易的。个人很喜欢这道题,既用到了别的题目,又有动态规划的思想,复杂度还非常漂亮,又一次体现了算法的魅力哈。

2014年4月23日星期三

Construct Binary Tree from Inorder and Postorder Traversal -- LeetCode

原题链接: http://oj.leetcode.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/
这道题和Construct Binary Tree from Preorder and Inorder Traversal思路完全一样,如果有朋友不了解,请先看看Construct Binary Tree from Preorder and Inorder Traversal哈。这里的区别是要从中序遍历和后序遍历中构造出树,算法还是一样,只是现在取根是从后面取(因为后序遍历根是遍历的最后一个元素)。思想和代码基本都是差不多的,自然时间复杂度和空间复杂度也还是O(n)。代码如下:
public TreeNode buildTree(int[] inorder, int[] postorder) {
    if(inorder==null || postorder==null || inorder.length==0 || postorder.length==0)
    {
        return null;
    }
    HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
    for(int i=0;i<inorder.length;i++)
    {
        map.put(inorder[i],i);
    }
    return helper(inorder,postorder,0,inorder.length-1, 0, postorder.length-1,map);
}
private TreeNode helper(int[] inorder, int[] postorder, int inL, int inR, int postL, int postR, HashMap<Integer, Integer> map)
{
    if(inL>inR || postL>postR)
        return null;
    TreeNode root = new TreeNode(postorder[postR]);
    int index = map.get(root.val);
    root.left = helper(inorder,postorder,inL,index-1,postL,postL+index-inL-1,map);
    root.right = helper(inorder,postorder,index+1,inR,postR-(inR-index),postR-1,map);
    return root;
}
这道题和Construct Binary Tree from Preorder and Inorder Traversal是树中难度比较大的题目了,有朋友可能会想根据先序遍历和后序遍历能不能重新构造出树来,答案是否定的。只有中序便利可以根据根的位置切开左右子树,其他两种遍历都不能做到,其实先序遍历和后序遍历是不能唯一确定一棵树的,会有歧义发生,也就是两棵不同的树可以有相同的先序遍历和后序遍历,有兴趣的朋友可以试试举出这种例子哈。

Construct Binary Tree from Preorder and Inorder Traversal -- LeetCode

原题链接: http://oj.leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
这道题是树中比较有难度的题目,需要根据先序遍历和中序遍历来构造出树来。这道题看似毫无头绪,其实梳理一下还是有章可循的。下面我们就用一个例子来解释如何构造出树。
假设树的先序遍历是12453687,中序遍历是42516837。这里最重要的一点就是先序遍历可以提供根的所在,而根据中序遍历的性质知道根的所在就可以将序列分为左右子树。比如上述例子,我们知道1是根,所以根据中序遍历的结果425是左子树,而6837就是右子树。接下来根据切出来的左右子树的长度又可以在先序便利中确定左右子树对应的子序列(先序遍历也是先左子树后右子树)。根据这个流程,左子树的先序遍历和中序遍历分别是245和425,右子树的先序遍历和中序遍历则是3687和6837,我们重复以上方法,可以继续找到根和左右子树,直到剩下一个元素。可以看出这是一个比较明显的递归过程,对于寻找根所对应的下标,我们可以先建立一个HashMap,以免后面需要进行线行搜索,这样每次递归中就只需要常量操作就可以完成对根的确定和左右子树的分割。
算法最终相当于一次树的遍历,每个结点只会被访问一次,所以时间复杂度是O(n)。而空间我们需要建立一个map来存储元素到下标的映射,所以是O(n)。代码如下:
public TreeNode buildTree(int[] preorder, int[] inorder) {
    if(preorder==null || inorder==null)
        return null;
    HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
    for(int i=0;i<inorder.length;i++)
    {
        map.put(inorder[i],i);
    }
    return helper(preorder,0,preorder.length-1,inorder,0,inorder.length-1, map);
}
private TreeNode helper(int[] preorder, int preL, int preR, int[] inorder, int inL, int inR, HashMap<Integer, Integer> map)
{
    if(preL>preR || inL>inR)
        return null;
    TreeNode root = new TreeNode(preorder[preL]);
    int index = map.get(root.val);
    root.left = helper(preorder, preL+1, index-inL+preL, inorder, inL, index-1, map);
    root.right = helper(preorder, preL+index-inL+1, preR, inorder, index+1, inR,map);
    return root;
}
可以看出上面实现结果还是非常接近于一次树的遍历的,只是我们是以一个构造树的形式,在遍历中把树创建出来。这种题目算是树中的难题了,不过理清思路,其实也不过如此哈~

Remove Duplicates from Sorted List II -- LeetCode

原题链接: http://oj.leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
这道题跟Remove Duplicates from Sorted List比较类似,只是这里要把出现重复的元素全部删除。其实道理还是一样,只是现在要把前驱指针指向上一个不重复的元素中,如果找到不重复元素,则把前驱指针知道该元素,否则删除此元素。算法只需要一遍扫描,时间复杂度是O(n),空间只需要几个辅助指针,是O(1)。代码如下:
public ListNode deleteDuplicates(ListNode head) {
    if(head == null)
        return head;
    ListNode helper = new ListNode(0);
    helper.next = head;
    ListNode pre = helper;
    ListNode cur = head;
    while(cur!=null)
    {
        while(cur.next!=null && pre.next.val==cur.next.val)
        {
            cur = cur.next;
        }
        if(pre.next==cur)
        {
            pre = pre.next;
        }
        else
        {
            pre.next = cur.next;
        }
        cur = cur.next;
    }
    
    return helper.next;
}
可以看到,上述代码中我们创建了一个辅助的头指针,是为了修改链表头的方便。前面介绍过,一般会改到链表头的题目就会需要一个辅助指针,是比较常见的技巧。

2014年4月22日星期二

Remove Duplicates from Sorted List -- LeetCode

原题链接: http://oj.leetcode.com/problems/remove-duplicates-from-sorted-list/
这是一道比较简单的链表操作的题目,要求是删去有序链表中重复的元素。方法比较清晰,维护两个指针,一个指向当前不重复的最后一个元素,一个进行依次扫描,遇到不重复的则更新第一个指针,继续扫描,否则就把前面指针指向当前元素的下一个(即把当前元素从链表中删除)。时间上只需要一次扫描,所以是O(n),空间上两个额外指针,是O(1)。代码如下:
public ListNode deleteDuplicates(ListNode head) {
    if(head == null)
        return head;
    ListNode pre = head;
    ListNode cur = head.next;
    while(cur!=null)
    {
        if(cur.val == pre.val)
            pre.next = cur.next;
        else    
            pre = cur;
        cur = cur.next;
    }
    return head;
}
链表操作在LeetCode中占有一定的比例,不过面试中出现的频率并不是特别高,不过基本的操作还是要熟练的。这道题目还可以求于另一个数据结构数组中,也比较简单,有兴趣可以看看Remove Duplicates from Sorted Array

Remove Duplicates from Sorted Array II -- LeetCode

原题链接: http://oj.leetcode.com/problems/remove-duplicates-from-sorted-array-ii/
这道题跟Remove Duplicates from Sorted Array比较类似,区别只是这里元素可以重复出现至多两次,而不是一次。其实也比较简单,只需要维护一个counter,当counter是2时,就直接跳过即可,否则说明元素出现次数没有超,继续放入结果数组,若遇到新元素则重置counter。总体算法只需要扫描一次数组,所以时间上是O(n),空间上只需要维护一个index和counter,所以是O(1)。代码如下:
public int removeDuplicates(int[] A) {
    if(A==null || A.length==0)
        return 0;
    int idx = 0;
    int count = 0;
    for(int i=0;i<A.length;i++)
    {
        if(i>0 && A[i]==A[i-1])
        {
            count++;
            if(count>=3)
                continue;
        }
        else
        {
            count = 1;
        }
        A[idx++]=A[i];
    }
    return idx;
}
这种简单数组操作的问题在电面中比较常见,既然简单,所以出手就要稳,不能出错,还是要保证无误哈。

Word Search -- LeetCode

原题链接: http://oj.leetcode.com/problems/word-search/
这道题很容易感觉出来是图的题目,其实本质上还是做深度优先搜索。基本思路就是从某一个元素出发,往上下左右深度搜索是否有相等于word的字符串。这里注意每次从一个元素出发时要重置访问标记(也就是说虽然单次搜索字符不能重复使用,但是每次从一个新的元素出发,字符还是重新可以用的)。深度优先搜索的算法就不再重复解释了,不了解的朋友可以看看wiki - 深度优先搜索。我们知道一次搜索的复杂度是O(E+V),E是边的数量,V是顶点数量,在这个问题中他们都是O(m*n)量级的(因为一个顶点有固定上下左右四条边)。加上我们对每个顶点都要做一次搜索,所以总的时间复杂度最坏是O(m^2*n^2),空间上就是要用一个数组来记录访问情况,所以是O(m*n)。代码如下:
public boolean exist(char[][] board, String word) {
    if(word==null || word.length()==0)
        return true;
    if(board==null || board.length==0 || board[0].length==0)
        return false;
    boolean[][] used = new boolean[board.length][board[0].length];
    for(int i=0;i<board.length;i++)
    {
        for(int j=0;j<board[0].length;j++)
        {
            if(search(board,word,0,i,j,used))
                return true;
        }
    }
    return false;
}
private boolean search(char[][] board, String word, int index, int i, int j, boolean[][] used)
{
    if(index == word.length())
        return true;
    if(i<0 || j<0 || i>=board.length || j>=board[0].length || used[i][j] || board[i][j]!=word.charAt(index))
        return false;
    used[i][j] = true;
    boolean res = search(board,word,index+1,i-1,j,used) || search(board,word,index+1,i+1,j,used)
        || search(board,word,index+1,i,j-1,used) || search(board,word,index+1,i,j+1,used);
    used[i][j] = false;
    return res;
}
这道题其实还可以变一变,比如字符可以重复使用。准备的时候多联想还是比较好的,因为面试中常常会做完一道题会变一下问问,虽然经常不用重新写代码,但是想了解一下思路,有兴趣的朋友可以想想哈。

2014年4月21日星期一

Subsets -- LeetCode

原题链接: http://oj.leetcode.com/problems/subsets/
求子集问题是经典的NP问题,复杂度上我们就无法强求了哈,肯定是非多项式量级的。一般来说这个问题有两种解法:递归和非递归。
我们先来说说递归解法,主要递推关系就是假设函数返回递归集合,现在加入一个新的数字,我们如何得到包含新数字的所有子集。其实就是在原有的集合中对每集合中的每个元素都加入新元素得到子集,然后放入原有集合中(原来的集合中的元素不用删除,因为他们也是合法子集)。而结束条件就是如果没有元素就返回空集(注意空集不是null,而是没有元素的数组)就可以了。时间和空间都是取决于结果的数量,也就是O(2^n),代码如下:
public ArrayList<ArrayList<Integer>> subsets(int[] num) {
    if(num == null)
        return null;
    Arrays.sort(num);
    return helper(num, num.length-1);
}
private ArrayList<ArrayList<Integer>> helper(int[] num, int index)
{
    if(index == -1)
    {
        ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
        ArrayList<Integer> elem = new ArrayList<Integer>();
        res.add(elem);
        return res;
    }
    ArrayList<ArrayList<Integer>> res = helper(num,index-1);
    int size = res.size();
    for(int i=0;i<size;i++)
    {
        ArrayList<Integer> elem = new ArrayList<Integer>(res.get(i));
        elem.add(num[index]);
        res.add(elem);
    }
    return res;
}
其实非递归解法的思路和递归是一样的,只是没有回溯过程,也就是自无到有的一个个元素加进来,然后构造新的子集加入结果集中,代码如下:
public ArrayList<ArrayList<Integer>> subsets(int[] S) {
     ArrayList<ArrayList<Integer>> res = new  ArrayList<ArrayList<Integer>>();
     res.add(new ArrayList<Integer>());
     if(S == null || S.length == 0)
        return res;
    Arrays.sort(S);
    for(int i=0;i<S.length;i++)
    {
        int size = res.size();
        for(int j=0;j<size;j++)
        {
            ArrayList<Integer> item = new ArrayList<Integer>(res.get(j));
            item.add(S[i]);
            res.add(item);
        }
    }
    return res;
}
这种NP问题算法上都没有什么大的提高,基本上就是考察对于递归和非递归解法的理解,都是和N-Queens一个套路,掌握之后就没什么难度哈。

Sort Colors -- LeetCode

原题链接: http://oj.leetcode.com/problems/sort-colors/
这道题也是数组操作的题目,其实就是要将数组排序,只是知道数组中只有三个元素0,1,2。熟悉计数排序的朋友可能很快就发现这其实就是使用计数排序,元素空间只需要三个元素即可。代码如下:
public void sortColors(int[] A) {
    if(A==null || A.length==0)
        return;
    int[] res = new int[A.length];
    int[] helper = new int[3];
    for(int i=0;i<A.length;i++)
    {
        helper[A[i]]++;
    }
    for(int i=1;i<3;i++)
    {
        helper[i]=helper[i]+helper[i-1];
    }
    for(int i=A.length-1;i>=0;i--)
    {
        res[helper[A[i]]-1] = A[i];
        helper[A[i]]--;
    }
    for(int i=0;i<A.length;i++)
    {
        A[i] = res[i];
    }
}
上面的代码是计数排序的标准解法,可以看到总共进行了三次扫描,其实最后一次只是把结果数组复制到原数组而已,如果不需要in-place的结果只需要两次扫描。
其实就算返回元素组也可以是两次扫描,这需要用到元素只有0,1,2的本质。我们知道helper[i]中是包含着0,1,2的元素数量,我们只需要按照helper[0,1,2]的数量依次赋值过来即可(每层循环把helper[i]--,如果helper[i]到0就i++就可以了),只是这样就不是计数排序比较标准的解法,我希望还是复习一下。
这种方法的时间复杂度是O(2*n),空间是O(k),k是颜色的数量,这里是3。
上述方法需要两次扫描,我们考虑怎么用一次扫描来解决。其实还是利用了颜色是三种这一点,道理其实也简单,就是搞两个指针,一个指在当前0的最后一个下标,另一个是指在当前1的最后一个下标(2不需要指针因为剩下的都是2了)。进行一次扫描,如果遇到0就两个指针都前进一步并进行赋值,如果遇到1就后一个指针前进一步并赋值。代码如下:
public void sortColors(int[] A) {
    if(A==null || A.length==0)
        return;
    int idx0 = 0;
    int idx1 = 0;
    for(int i=0;i<A.length;i++)
    {
        if(A[i]==0)
        {
            A[i] = 2;
            A[idx1++] = 1;
            A[idx0++] = 0;
        }
        else if(A[i]==1)
        {
            A[i] = 2;
            A[idx1++] = 1;
        }
    }
}
上述方法时间复杂度还是O(n),只是只需要一次扫描,空间上是O(1)(如果颜色种类是已知的话)。
这道题我觉得主要还是熟悉一下计数排序计数排序是线性排序中比较重要的一种,关于排序要搞个专题专门的复习一下,很多排序的基本思想都对解题有帮助哈。

2014年4月20日星期日

Search a 2D Matrix -- LeetCode

原题链接: http://oj.leetcode.com/problems/search-a-2d-matrix/
这道题是二分查找Search Insert Position的题目,因为矩阵是行有序并且列有序,查找只需要先按行查找,定位出在哪一行之后再进行列查找即可,所以就是进行两次二分查找。时间复杂度是O(logm+logn),空间上只需两个辅助变量,因而是O(1),代码如下:
public boolean searchMatrix(int[][] matrix, int target) {
    if(matrix == null || matrix.length==0 || matrix[0].length==0)
        return false;
    int l = 0;
    int r = matrix.length-1;
    while(l<=r)
    {
        int mid = (l+r)/2;
        if(matrix[mid][0] == target) return true;
        if(matrix[mid][0] > target)
        {
            r = mid-1;
        }
        else
        {
            l = mid+1;
        }
    }
    int row = r;
    if(row<0)
        return false;
    l = 0;
    r = matrix[0].length-1;
    while(l<=r)
    {
        int mid = (l+r)/2;
        if(matrix[row][mid] == target) return true;
        if(matrix[row][mid] > target)
        {
            r = mid-1;
        }
        else
        {
            l = mid+1;
        }
    }   
    return false;
}
二分查找是面试中出现频率不低的问题,但是很少直接考二分查找,会考一些变体,除了这道题,还有Search in Rotated Sorted ArraySearch for a Range,思路其实差不多,稍微变化一下即可,有兴趣可以练习一下哈。

Edit Distance -- LeetCode

原题链接: http://oj.leetcode.com/problems/edit-distance/
这道题求一个字符串编辑成为另一个字符串的最少操作数,操作包括添加,删除或者替换一个字符。这道题难度是比较大的,用常规思路出来的方法一般都是brute force,而且还不一定正确。这其实是一道二维动态规划的题目,模型上确实不容易看出来,下面我们来说说递推式。
我们维护的变量res[i][j]表示的是word1的前i个字符和word2的前j个字符编辑的最少操作数是多少。假设我们拥有res[i][j]前的所有历史信息,看看如何在常量时间内得到当前的res[i][j],我们讨论两种情况:
1)如果word1[i-1]=word2[j-1],也就是当前两个字符相同,也就是不需要编辑,那么很容易得到res[i][j]=res[i-1][j-1],因为新加入的字符不用编辑;
2)如果word1[i-1]!=word2[j-1],那么我们就考虑三种操作,如果是插入word1,那么res[i][j]=res[i-1][j]+1,也就是取word1前i-1个字符和word2前j个字符的最好结果,然后添加一个插入操作;如果是插入word2,那么res[i][j]=res[i][j-1]+1,道理同上面一种操作;如果是替换操作,那么类似于上面第一种情况,但是要加一个替换操作(因为word1[i-1]和word2[j-1]不相等),所以递推式是res[i][j]=res[i-1][j-1]+1。上面列举的情况包含了所有可能性,有朋友可能会说为什么没有删除操作,其实这里添加一个插入操作永远能得到与一个删除操作相同的效果,所以删除不会使最少操作数变得更好,因此如果我们是正向考虑,则不需要删除操作。取上面几种情况最小的操作数,即为第二种情况的结果,即res[i][j] = min(res[i-1][j], res[i][j-1], res[i-1][j-1])+1。
接下来就是分析复杂度,算法时间上就是两层循环,所以时间复杂度是O(m*n),空间上每一行只需要上一行信息,所以可以只用一维数组即可,我们取m, n中小的放入内层循环,则复杂度是O(min(m,n))。代码如下:
public int minDistance(String word1, String word2) {
    if(word1.length()==0)
        return word2.length();
    if(word2.length()==0)
        return word1.length();
    String minWord = word1.length()>word2.length()?word2:word1;
    String maxWord = word1.length()>word2.length()?word1:word2;
    int[] res = new int[minWord.length()+1];
    for(int i=0;i<=minWord.length();i++)
    {
        res[i] = i;
    }
    for(int i=0;i<maxWord.length();i++)
    {
        int[] newRes = new int[minWord.length()+1];
        newRes[0] = i+1;
        for(int j=0;j<minWord.length();j++)
        {
            if(minWord.charAt(j)==maxWord.charAt(i))
            {
                newRes[j+1]=res[j];
            }
            else
            {
                newRes[j+1] = Math.min(res[j],Math.min(res[j+1],newRes[j]))+1;
            }
        }
        res = newRes;
    }
    return res[minWord.length()];
}
上面代码用了minWord, maxWord是为了使得空间是min(m,n),细节做得比较细,面试的时候可能不用刻意这么做,提一下就好。
这道题目算是难度比较大的题目,所以在短时间的面试可能时间太紧了,所以也有变体。我自己在面试Google的时候,问的是如何判断edit distance是不是在1以内,返回true或false就可以了。这样一改其实就没有必要动态规划了,只需要利用距离只有1这一点进行判断就可以,大概思路就是只要有一个不同,接下来就不能再有不同了,有兴趣的朋友可以想想哈。

2014年4月18日星期五

Set Matrix Zeroes -- LeetCode

原题链接: http://oj.leetcode.com/problems/set-matrix-zeroes/
这是一个矩阵操作的题目,目标很明确,就是如果矩阵如果有元素为0,就把对应的行和列上面的元素都置为0。这里最大的问题就是我们遇到0的时候不能直接把矩阵的行列在当前矩阵直接置0,否则后面还没访问到的会被当成原来是0,最后会把很多不该置0的行列都置0了。
一个直接的想法是备份一个矩阵,然后在备份矩阵上判断,在原矩阵上置0,这样当然是可以的,不过空间复杂度是O(m*n),不是很理想。
上面的方法如何优化呢?我们看到其实判断某一项是不是0只要看它对应的行或者列应不应该置0就可以,所以我们可以维护一个行和列的布尔数组,然后扫描一遍矩阵记录那一行或者列是不是应该置0即可,后面赋值是一个常量时间的判断。这种方法的空间复杂度是O(m+n)。
其实还可以再优化,我们考虑使用第一行和第一列来记录上面所说的行和列的置0情况,这里问题是那么第一行和第一列自己怎么办?想要记录它们自己是否要置0,只需要两个变量(一个是第一行,一个是第一列)就可以了。然后就是第一行和第一列,如果要置0,就把它的值赋成0(反正它最终也该是0,无论第一行或者第一列有没有0),否则保留原值。然后根据第一行和第一列的记录对其他元素进行置0。最后再根据前面的两个标记来确定是不是要把第一行和第一列置0就可以了。这样的做法只需要两个额外变量,所以空间复杂度是O(1)。
时间上来说上面三种方法都是一样的,需要进行两次扫描,一次确定行列置0情况,一次对矩阵进行实际的置0操作,所以总的时间复杂度是O(m*n)。代码如下:
public void setZeroes(int[][] matrix) {
    if(matrix==null || matrix.length==0 || matrix[0].length==0)
        return;
    boolean rowFlag = false;
    boolean colFlag = false;
    for(int i=0;i<matrix.length;i++)
    {
        if(matrix[i][0]==0)
        {
            colFlag = true;
            break;
        }
    }
    for(int i=0;i<matrix[0].length;i++)
    {
        if(matrix[0][i]==0)
        {
            rowFlag = true;
            break;
        }
    }      
    for(int i=1;i<matrix.length;i++)
    {
        for(int j=1;j<matrix[0].length;j++)
        {
            if(matrix[i][j]==0)
            {
                matrix[i][0] = 0;
                matrix[0][j] = 0;
            }
        }
    }
    for(int i=1;i<matrix.length;i++)
    {
        for(int j=1;j<matrix[0].length;j++)
        {
            if(matrix[i][0]==0 || matrix[0][j]==0)
                matrix[i][j] = 0;
        }
    }
    if(colFlag)
    {
        for(int i=0;i<matrix.length;i++)
        {
            matrix[i][0] = 0;
        }
    }
    if(rowFlag)
    {
        for(int i=0;i<matrix[0].length;i++)
        {
            matrix[0][i] = 0;
        }
    }
}
这道题也是cc150里面比较经典的题目,看似比较简单,却可以重重优化,最终达到常量空间。其实面试中面试官看重的是对于算法时间空间复杂度的理解,对优化的概念,这些常常比题目本身的难度更加重要,平常做题还是要对这些算法分析多考虑哈。

Text Justification -- LeetCode

原题链接: http://oj.leetcode.com/problems/text-justification/
这道题属于纯粹的字符串操作,要把一串单词安排成多行限定长度的字符串。主要难点在于空格的安排,首先每个单词之间必须有空格隔开,而当当前行放不下更多的单词并且字符又不能填满长度L时,我们要把空格均匀的填充在单词之间。如果剩余的空格量刚好是间隔倍数那么就均匀分配即可,否则还必须把多的一个空格放到前面的间隔里面。实现中我们维护一个count计数记录当前长度,超过之后我们计算共同的空格量以及多出一个的空格量,然后将当行字符串构造出来。最后一个细节就是最后一行不需要均匀分配空格,句尾留空就可以,所以要单独处理一下。时间上我们需要扫描单词一遍,然后在找到行尾的时候在扫描一遍当前行的单词,不过总体每个单词不会被访问超过两遍,所以总体时间复杂度是O(n)。而空间复杂度则是结果的大小(跟单词数量和长度有关,不能准确定义,如果知道最后行数r,则是O(r*L))。代码如下:
public ArrayList<String> fullJustify(String[] words, int L) {
    ArrayList<String> res = new ArrayList<String>();
    if(words==null || words.length==0)
        return res;
    int count = 0;
    int last = 0;
    for(int i=0;i<words.length;i++)
    {
        if(count+words[i].length()+(i-last)>L)
        {
            int spaceNum = 0;
            int extraNum = 0;
            if(i-last-1>0)
            {
                spaceNum = (L-count)/(i-last-1);
                extraNum = (L-count)%(i-last-1);
            }
            StringBuilder str = new StringBuilder();
            for(int j=last;j<i;j++)
            {
                str.append(words[j]);
                if(j<i-1)
                {
                    for(int k=0;k<spaceNum;k++)
                    {
                        str.append(" ");
                    }
                    if(extraNum>0)
                    {
                        str.append(" ");
                    }
                    extraNum--;
                }
            }
            for(int j=str.length();j<L;j++)
            {
                str.append(" ");
            }       
            res.add(str.toString());
            count=0;
            last=i;
        }
        count += words[i].length();
    }
    StringBuilder str = new StringBuilder();
    for(int i=last;i<words.length;i++)
    {
        str.append(words[i]);
        if(str.length()<L)
            str.append(" ");
    }
    for(int i=str.length();i<L;i++)
    {
        str.append(" ");
    }
    res.add(str.toString());
    return res;
}
这道题属于那种文本编辑的子操作之类的题目,从算法思路上没有什么特别,不过实现细节还是比较多的,不容易一次做对,可能要多练习几次哈。

2014年4月17日星期四

Climbing Stairs -- LeetCode

原题链接: http://oj.leetcode.com/problems/climbing-stairs/
这道题目是求跑楼梯的可行解法数量。每一步可以爬一格或者两个楼梯,可以发现,递推式是f(n)=f(n-1)+f(n-2),也就是等于前一格的可行数量加上前两格的可行数量。熟悉的朋友可能发现了,这个递归式正是斐波那契数列的定义,不熟悉的朋友可以看看Wiki - 斐波那契数列。根据这个定义,其实很容易实现,可以用递归或者递推都是比较简单的,下面列举一下递推的代码:
public int climbStairs(int n) {
    int f1 = 1;
    int f2 = 2;
    if(n==1)
        return f1;
    if(n==2)
        return f2;
    for(int i=3;i<=n;i++)
    {
        int f3 = f1+f2;
        f1 = f2;
        f2 = f3;
    }
    return f2;
}
可以很容易判断,上面代码的时间复杂度是O(n),面试一般都会实现一下,不过还没完,面试官会接着问一下,有没有更好的解法?还真有,斐波那契数列其实是有O(logn)的解法的。根据wiki我们知道,斐波那契数列是有通项公式的,如下:
所以如果我们用Pow(x, n)中介绍的分治法来求解这个n次幂的话可以完成O(logn)的求解。还有另一种理解方法就是斐波那契数列的线性代数解法(参见Wiki - 斐波那契数列),可以看到迭代是一个二乘二的简单矩阵,数列的第n个数就是求解这个矩阵的n-2次幂,同样用分治法就可以完成O(logn)的求解。
这是对于斐波那契数列问题的一般面试过程,先实现一下通常的O(n)的解法,然后再了解一下是否知道有O(logn)的解法,一般不要求实现,知道就行,不过其实实现也不是很难,有兴趣的朋友可以练习一下哈。

Simplify Path -- LeetCode

原题链接: http://oj.leetcode.com/problems/simplify-path/
这道题目是Linux内核中比较常见的一个操作,就是对一个输入的文件路径进行简化。思路比较明确,就是维护一个栈,对于每一个块(以‘/’作为分界)进行分析,如果遇到‘../’则表示要上一层,那么就是进行出栈操作,如果遇到‘./’则是停留当前,直接跳过,其他文件路径则直接进栈即可。最后根据栈中的内容转换成路径即可(这里是把栈转成数组,然后依次添加)。时间上不会超过两次扫描(一次是进栈得到简化路径,一次是出栈获得最后结果),所以时间复杂度是O(n),空间上是栈的大小,也是O(n)。代码如下:
public String simplifyPath(String path) {
    if(path == null || path.length()==0)
    {
        return "";
    }
    LinkedList<String> stack = new LinkedList<String>();
    StringBuilder res = new StringBuilder();
    int i=0;
    
    while(i<path.length())
    {
        int index = i;
        StringBuilder temp = new StringBuilder();
        while(i<path.length() && path.charAt(i)!='/')
        {
            temp.append(path.charAt(i));
            i++;
        }
        if(index!=i)
        {
            String str = temp.toString();
            if(str.equals(".."))
            {
                if(!stack.isEmpty())
                    stack.pop();
            }
            else if(!str.equals("."))
            {
                stack.push(str);
            }
        }
        i++;
    }
    if(!stack.isEmpty())
    {
        String[] strs = stack.toArray(new String[stack.size()]);
        for(int j=strs.length-1;j>=0;j--)
        {
          res.append("/"+strs[j]);
        }
    }
    if(res.length()==0)
        return "/";
    return res.toString();
}
这道题其实还有一种做法,不需要维护栈,也就是不用额外空间,但是要对字符位置进行比较好的记录或者回溯,可能会多扫描一次,但是不会增加时间复杂度的量级。不过那个方法虽然对于空间上有提高,但是有很多细节的操作,并且没有什么算法思想,属于纯字符串操作,个人觉得意义不是很大,而且在面试中也很难正确写出来,还是比较推荐上述解法。

2014年4月16日星期三

Convert Sorted List to Binary Search Tree -- LeetCode

原题链接: http://oj.leetcode.com/problems/convert-sorted-list-to-binary-search-tree/
这个题是二分查找树的题目,要把一个有序链表转换成一棵二分查找树。其实原理还是跟Convert Sorted Array to Binary Search Tree这道题相似,我们需要取中点作为当前函数的根。这里的问题是对于一个链表我们是不能常量时间访问它的中间元素的。这时候就要利用到树的中序遍历了,按照递归中序遍历的顺序对链表结点一个个进行访问,而我们要构造的二分查找树正是按照链表的顺序来的。思路就是先对左子树进行递归,然后将当前结点作为根,迭代到下一个链表结点,最后在递归求出右子树即可。整体过程就是一次中序遍历,时间复杂度是O(n),总的空间复杂度是栈空间O(logn)。代码如下:
public TreeNode sortedListToBST(ListNode head) {
    if(head == null)
        return null;
    ListNode cur = head;
    int count = 0;
    while(cur!=null)
    {
        cur = cur.next;
        count++;
    }
    ArrayList<ListNode> list = new ArrayList<ListNode>();
    list.add(head);
    return helper(list,0,count-1);
}
private TreeNode helper(ArrayList<ListNode> list, int l, int r)
{
    if(l>r)
        return null;
    int m = (l+r)/2;
    TreeNode left = helper(list,l,m-1);
    TreeNode root = new TreeNode(list.get(0).val);
    root.left = left;
    list.set(0,list.get(0).next);
    root.right = helper(list,m+1,r);
    return root;
}
这道题是不错的题目,不过这种构造的方式比较绕,因为一般来说我们都是对于存在的树进行遍历,这里是模拟一个中序遍历的过程把树从无到有地构造出来。过程比较不常规,不过多想想就明白了哈。

Convert Sorted Array to Binary Search Tree -- LeetCode

原题链接: http://oj.leetcode.com/problems/convert-sorted-array-to-binary-search-tree/
这道题是二分查找树的题目,要把一个有序数组转换成一颗二分查找树。其实从本质来看,如果把一个数组看成一棵树(也就是以中点为根,左右为左右子树,依次下去)数组就等价于一个二分查找树。所以如果要构造这棵树,那就是把中间元素转化为根,然后递归构造左右子树。所以我们还是用二叉树递归的方法来实现,以根作为返回值,每层递归函数取中间元素,作为当前根和赋上结点值,然后左右结点接上左右区间的递归函数返回值。时间复杂度还是一次树遍历O(n),空间复杂度是栈空间O(logn)加上结果的空间O(n),所以额外空间是O(logn),总体是O(n)。代码如下:
public TreeNode sortedArrayToBST(int[] num) {
    if(num==null || num.length==0)
        return null;
    return helper(num,0,num.length-1);
}
private TreeNode helper(int[] num, int l, int r)
{
    if(l>r)
        return null;
    int m = (l+r)/2;
    TreeNode root = new TreeNode(num[m]);
    root.left = helper(num,l,m-1);
    root.right = helper(num,m+1,r);
    return root;
}
这是一道不错的题目,模型简单,但是考察了遍历和二分查找树的数据结构,比较适合在电面中出现,类似的题目有Convert Sorted List to Binary Search Tree,是这道题非常好的后续问题,不同数据结构,实现上也会有所不同,有兴趣的朋友可以看看哈。

2014年4月15日星期二

Validate Binary Search Tree -- LeetCode

原题链接: http://oj.leetcode.com/problems/validate-binary-search-tree/
这道题是检查一颗二分查找树是否合法,二分查找树是非常常见的一种数据结构,因为它可以在O(logn)时间内实现搜索。这里我们介绍两种方法来解决这个问题。
第一种是利用二分查找树的性质,就是它的中序遍历结果是按顺序递增的。根据这一点我们只需要中序遍历这棵树,然后保存前驱结点,每次检测是否满足递增关系即可。注意以下代码我么用一个一个变量的数组去保存前驱结点,原因是java没有传引用的概念,如果传入一个变量,它是按值传递的,所以是一个备份的变量,改变它的值并不能影响它在函数外部的值,算是java中的一个小细节。代码如下:
public boolean isValidBST(TreeNode root) {
    ArrayList<Integer> pre = new ArrayList<Integer>();
    pre.add(null);
    return helper(root, pre);
}
private boolean helper(TreeNode root, ArrayList<Integer> pre)
{
    if(root == null)
        return true;
    boolean left = helper(root.left,pre);
    if(pre.get(0)!=null && root.val<=pre.get(0))
        return false;
    pre.set(0,root.val);
    return left && helper(root.right,pre);
}
第二种方法是根据题目中的定义来实现,其实就是对于每个结点保存左右界,也就是保证结点满足它的左子树的每个结点比当前结点值小,右子树的每个结点比当前结点值大。对于根节点不用定位界,所以是无穷小到无穷大,接下来当我们往左边走时,上界就变成当前结点的值,下界不变,而往右边走时,下界则变成当前结点值,上界不变。如果在递归中遇到结点值超越了自己的上下界,则返回false,否则返回左右子树的结果。代码如下:
public boolean isValidBST(TreeNode root) {
    return helper(root, Integer.MIN_VALUE, Integer.MAX_VALUE); 
}
boolean helper(TreeNode root, int min, int max)   
{  
    if(root == null)  
       return true;  
    if(root.val <= min || root.val >= max)
         return false;  
     return helper(root.left, min, root.val) && helper(root.right, root.val, max);
}  
上述两种方法本质上都是做一次树的遍历,所以时间复杂度是O(n),空间复杂度是O(logn)。个人其实更喜欢第一种做法,因为思路简单,而且不需要用到整数最大值和最小值这种跟语言相关的量来定义无穷大和无穷小。
二分查找树在面试中非常常见,因为它既是树的数据结构,又带有一些特殊的性质,并且模型简单,很适合在面试中对基本的数据结构和算法进行考核,还是尽量要对二分查找树理解比较透彻哈。

Valid Number -- LeetCode

原题链接: http://oj.leetcode.com/problems/valid-number/
这是一道检查字符串输入是否为合法的题目。基本规则是按照科学计数法,所以会出现的特殊字符有以下几个:符号位‘+’,‘-’,小数点‘.’,还有‘e’和‘E’,剩下的就只有数字0-9了,其他字符如果出现就是非法字符,返回false。数字字符在哪里出现都是ok的,我们主要考虑几个特殊字符的情况。
对于小数点出现的时候,我们要满足一下这些条件:(1)前面不能有小数点或者‘e’和‘E’;(2)前一位是数字(不能是第一位)或者后一位要是数字(不能是最后一位)。
对于正负号出现的情况,要满足条件:(1)必须是第一位或者在‘e’和‘E’后一位;(2)后一位要是数字。
对于‘e’和‘E’的情况,要满足:(1)前面不能有‘e’和‘E’出现过;(2)不能是第一位(前面没数字科学计数没有意义)或者最后一位(后面没数字就不用写指数了)。
根据上面列举的情况,我们用两个标签和做前后位的判断来实现,算法复杂度比较明显是O(n)的,只需要O(1)的额外空间。代码如下:
public boolean isNumber(String s) {
    if(s==null)
        return false;
    s = s.trim();
    if(s.length()==0)
        return false;
    boolean dotFlag = false;
    boolean eFlag = false;
    for(int i=0;i<s.length();i++)
    {
        switch(s.charAt(i))
        {
            case '.':
                if(dotFlag || eFlag 
                || ((i==0||!(s.charAt(i-1)>='0'&&s.charAt(i-1)<='9')) 
                    && (i==s.length()-1||!(s.charAt(i+1)>='0'&&s.charAt(i+1)<='9'))))
                    return false;
                dotFlag = true;
                break;
            case '+':
            case '-':
                if((i>0 && (s.charAt(i-1)!='e' && s.charAt(i-1)!='E'))
                  || (i==s.length()-1 || !(s.charAt(i+1)>='0'&&s.charAt(i+1)<='9'||s.charAt(i+1)=='.')))
                    return false;
                break;
            case 'e':
            case 'E':
                if(eFlag || i==s.length()-1 || i==0)
                    return false;
                eFlag = true;
                break;
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                break;
            default:
                return false;
        }
    }
    return true;
}
这种题目一般在面试tester职位中比较常见,需要考虑比较多的边界情况,更多的是在寻找bug的感觉。

2014年4月14日星期一

Balanced Binary Tree -- LeetCode

原题链接: http://oj.leetcode.com/problems/balanced-binary-tree/
这道题是树操作的题目,还是老套路用递归。这道题是求解树是否平衡,还是有一些小技巧的。要判断树是否平衡,根据题目的定义,深度是比需的信息,所以我们必须维护深度,另一方面我们又要返回是否为平衡树,那么对于左右子树深度差的判断也是必要的。这里我们用一个整数来做返回值,而0或者正数用来表示树的深度,而-1则用来比较此树已经不平衡了,如果已经不平衡,则递归一直返回-1即可,也没有继续比较的必要了,否则就利用返回的深度信息看看左右子树是不是违反平衡条件,如果违反返回-1,否则返回左右子树深度大的加一作为自己的深度即可。算法的时间是一次树的遍历O(n),空间是栈高度O(logn)。代码如下:
public boolean isBalanced(TreeNode root)
{
    return helper(root)>=0;
}
private int helper(TreeNode root)
{
    if(root == null)
        return 0;
    int left = helper(root.left);
    int right = helper(root.right);
    if(left<0 || right<0)
        return -1;
    if(Math.abs(left-right)>=2)
        return -1;
    return Math.max(left,right)+1;
}
可以看出树的题目万变不离其宗,都是递归遍历,只是处理上保存量,递归条件和结束条件会有一些变化。

Flatten Binary Tree to Linked List -- LeetCode

原题链接: http://oj.leetcode.com/problems/flatten-binary-tree-to-linked-list/
这是树的题目,要求把一颗二叉树按照先序遍历顺序展成一个链表,不过这个链表还是用树的结果,就是一直往右走(没有左孩子)来模拟链表。老套路还是用递归来解决,维护先序遍历的前一个结点pre,然后每次把pre的左结点置空,右结点设为当前结点。这里需要注意的一个问题就是我们要先把右子结点保存一下,以便等会可以进行递归,否则有可能当前结点的右结点会被覆盖,后面就取不到了。算法的复杂度时间上还是一次遍历,O(n)。空间上是栈的大小,O(logn)。代码如下:
public void flatten(TreeNode root) {
    ArrayList<TreeNode> pre = new ArrayList<TreeNode>();
    pre.add(null);
    helper(root, pre);
}
private void helper(TreeNode root, ArrayList<TreeNode> pre)
{
    if(root == null)
        return;
    TreeNode right = root.right;
    if(pre.get(0)!=null)
    {
        pre.get(0).left = null;
        pre.get(0).right = root;
    }
    pre.set(0,root);
    helper(root.left, pre);
    helper(right, pre);
}
树的题目讨论得比较多了,主要思路就是递归,考虑好递归条件和结束条件,有时候如果递归过程会对树结构进行修改的话,要先保存一下结点。

2014年4月13日星期日

Path Sum II -- LeetCode

原题链接: http://oj.leetcode.com/problems/path-sum-ii/
这道题是树的题目,跟Path Sum的要求很接近,都是寻找从根到叶子的路径。这道题目的要求是求所有满足条件的路径,所以我们需要数据结构来维护中间路径结果以及保存满足条件的所有路径。这里的时间复杂度仍然只是一次遍历O(n),而空间复杂度则取决于满足条件的路径和的数量(假设是k条),则空间是O(klogn)。代码如下:
public ArrayList<ArrayList<Integer>> pathSum(TreeNode root, int sum) {
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    if(root==null)
        return res;
    ArrayList<Integer> item = new ArrayList<Integer>();
    item.add(root.val);
    helper(root,sum-root.val,item,res);
    return res;
}
private void helper(TreeNode root, int sum, ArrayList<Integer> item, ArrayList<ArrayList<Integer>> res)
{
    if(root == null)
        return;
    if(root.left==null && root.right==null && sum==0)
    {
        res.add(new ArrayList<Integer>(item));
        return;
    }
    if(root.left!=null)
    {
        item.add(root.left.val);
        helper(root.left,sum-root.left.val,item,res);
        item.remove(item.size()-1);
    }
    if(root.right!=null)
    {
        item.add(root.right.val);
        helper(root.right,sum-root.right.val,item,res);
        item.remove(item.size()-1);
    }        
}
这类求解树的路径的题目是一种常见题型,类似的有Binary Tree Maximum Path Sum,那道题更加复杂一些,路径可以起始和结束于任何结点(把二叉树看成无向图),有兴趣的朋友可以看看哈。

Path Sum -- LeetCode

原题链接: http://oj.leetcode.com/problems/path-sum/
这道题是树操作的题目,判断是否从根到叶子的路径和跟给定sum相同的。还是用常规的递归方法来做,递归条件是看左子树或者右子树有没有满足条件的路径,也就是子树路径和等于当前sum减去当前节点的值。结束条件是如果当前节点是空的,则返回false,如果是叶子,那么如果剩余的sum等于当前叶子的值,则找到满足条件的路径,返回true。算法的复杂度是输的遍历,时间复杂度是O(n),空间复杂度是O(logn)。代码如下:
public boolean hasPathSum(TreeNode root, int sum) {
    if(root == null)
        return false;
    if(root.left == null && root.right==null && root.val==sum)
        return true;
    return hasPathSum(root.left, sum-root.val) || hasPathSum(root.right, sum-root.val);
}
树的题目在LeetCode中出现的频率很高,不过方法都很接近,掌握了就都差不多。这类求解树的路径的题目是一种常见题型,类似的还有Binary Tree Maximum Path Sum,那道题更加复杂一些,路径可以起始和结束于任何结点(把二叉树看成无向图),有兴趣的朋友可以看看哈。

2014年4月12日星期六

Distinct Subsequences -- LeetCode

原题链接: http://oj.leetcode.com/problems/distinct-subsequences/
这道题应该很容易感觉到是动态规划的题目。还是老套路,先考虑我们要维护什么量。这里我们维护res[i][j],对应的值是S的前i个字符和T的前j个字符有多少个可行的序列(注意这道题是序列,不是子串,也就是只要字符按照顺序出现即可,不需要连续出现)。下面来看看递推式,假设我们现在拥有之前的历史信息,我们怎么在常量操作时间内得到res[i][j]。假设S的第i个字符和T的第j个字符不相同,那么就意味着res[i][j]的值跟res[i-1][j]是一样的,前面该是多少还是多少,而第i个字符的加入也不会多出来任何可行结果。如果S的第i个字符和T的第j个字符相同,那么所有res[i-1][j-1]中满足的结果都会成为新的满足的序列,当然res[i-1][j]的也仍是可行结果,所以res[i][j]=res[i-1][j-1]+res[i-1][j]。所以综合上面两种情况,递推式应该是res[i][j]=(S[i]==T[j]?res[i-1][j-1]:0)+res[i-1][j]。算法进行两层循环,时间复杂度是O(m*n),而空间上只需要维护当前i对应的数据就可以,也就是O(m)。代码如下:
public int numDistinct(String S, String T) {
    if(T.length()==0)
    {
        return 1;
    }
    if(S.length()==0)
        return 0;
    int[] res = new int[T.length()+1];
    res[0] = 1;
    for(int i=0;i<S.length();i++)
    {
        for(int j=T.length()-1;j>=0;j--)
        {
            res[j+1] = (S.charAt(i)==T.charAt(j)?res[j]:0)+res[j+1];
        }
    }
    return res[T.length()];
}
可以看到代码跟上面推导的递推式下标有点不同,因为下标从0开始,这种细节在实现的时候比较能想清楚,这里res[j+1]相当于T的前j个字符对应的串,少看一个。而res[0]表示一个字符都没有时的结果,最后结果返回res[T.length()],对应于整个字符串的可行序列的数量。

2014年4月11日星期五

Populating Next Right Pointers in Each Node II -- LeetCode

原题链接: http://oj.leetcode.com/problems/populating-next-right-pointers-in-each-node-ii/
这道题目的要求和Populating Next Right Pointers in Each Node是一样的,只是这里的二叉树没要求是完全二叉树。其实在实现Populating Next Right Pointers in Each Node的时候我们已经兼容了不是完全二叉树的情况,其实也比较好实现,就是在判断队列结点时判断一下他的左右结点是否存在就可以了。具体算法就不再分析了,不熟悉的朋友可以看看Populating Next Right Pointers in Each Node。时间复杂度和空间复杂度不变,还是O(n)和O(1)。代码如下:
public void connect(TreeLinkNode root) {
    if(root == null)
        return;
    TreeLinkNode lastHead = root;
    TreeLinkNode pre = null;
    TreeLinkNode curHead = null;
    while(lastHead!=null)
    {
        TreeLinkNode lastCur = lastHead;
        while(lastCur != null)
        {
            if(lastCur.left!=null)
            {
                if(curHead == null)
                {
                    curHead = lastCur.left;
                    pre = curHead;
                }
                else
                {
                    pre.next = lastCur.left;
                    pre = pre.next;
                }
            }
            if(lastCur.right!=null)
            {
                if(curHead == null)
                {
                    curHead = lastCur.right;
                    pre = curHead;
                }
                else
                {
                    pre.next = lastCur.right;
                    pre = pre.next;
                }
            }                
            lastCur = lastCur.next;

        }
        lastHead = curHead;
        curHead = null;
    }
}
这道题本质是树的层序遍历,只是队列改成用结点自带的链表结点来维护。

Populating Next Right Pointers in Each Node -- LeetCode

原题链接: http://oj.leetcode.com/problems/populating-next-right-pointers-in-each-node/
这道题是要将一棵树的每一层维护成一个链表,树本身是给定的。其实思路上很接近层序遍历Binary Tree Level Order Traversal,只是这里不需要额外维护一个队列。因为这里每一层我们会维护成一个链表,这个链表其实就是每层起始的那个队列,因此我们只需要维护一个链表的起始指针就可以依次得到整个队列了。接下来就是有左加左入链表,有右加右入链表,知道链表没有元素了说明到达最底层了。算法的复杂度仍然是对每个结点访问一次,所以是O(n),而空间上因为不需要额外空间来存储队列了,所以是O(1)。代码如下:
public void connect(TreeLinkNode root) {
    if(root == null)
        return;
    TreeLinkNode lastHead = root;
    TreeLinkNode pre = null;
    TreeLinkNode curHead = null;
    while(lastHead!=null)
    {
        TreeLinkNode lastCur = lastHead;
        while(lastCur != null)
        {
            if(lastCur.left!=null)
            {
                if(curHead == null)
                {
                    curHead = lastCur.left;
                    pre = curHead;
                }
                else
                {
                    pre.next = lastCur.left;
                    pre = pre.next;
                }
            }
            if(lastCur.right!=null)
            {
                if(curHead == null)
                {
                    curHead = lastCur.right;
                    pre = curHead;
                }
                else
                {
                    pre.next = lastCur.right;
                    pre = pre.next;
                }
            }                
            lastCur = lastCur.next;

        }
        lastHead = curHead;
        curHead = null;
    }
}
这道题是树的层序遍历Binary Tree Level Order Traversal的扩展,操作上会更加繁琐一些,因为是通过维护层链表来完成遍历,不过本质上还是一次广度优先搜索。

2014年4月10日星期四

Binary Tree Level Order Traversal II -- LeetCode

原题链接: http://oj.leetcode.com/problems/binary-tree-level-order-traversal-ii/
这道题和Binary Tree Level Order Traversal很类似,都是层序遍历一棵树,只是这道题要求从最底层往最上层遍历。这道题我没有想到什么好的做法可以一次的自底向上进行层序遍历,能想到的就是进行Binary Tree Level Order Traversal中的遍历,然后对结果进行一次reverse。时间上和空间上仍是O(n)。代码如下:
public ArrayList<ArrayList<Integer>> levelOrderBottom(TreeNode root) {
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    if(root == null)
        return res;
    LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
    int lastNum = 1;
    int curNum = 0;
    int level = 0;
    queue.add(root);
    while(!queue.isEmpty())
    {
        TreeNode n = queue.pop();
        if(res.size()<=level)
            res.add(new ArrayList<Integer>());
        res.get(level).add(n.val);
        lastNum--;
        if(n.left!=null)
        {
            queue.add(n.left);
            curNum++;
        }
        if(n.right!=null)
        {
            queue.add(n.right);
            curNum++;
        }
        if(lastNum==0)
        {
            level++;
            lastNum = curNum;
            curNum = 0;
            
        }
    }
    Collections.reverse(res);
    return res;
}
上述做法确实没有什么特殊的地方,如果是这样做,那么这道题并没有什么考究,跟Binary Tree Level Order Traversal完全一样,没有太大意义。如果有朋友有更好的解法或者思路,欢迎交流哈,可以留言或者发邮件到linhuanmars@gmail.com给我哈。

Binary Tree Level Order Traversal -- LeetCode

原题链接: http://oj.leetcode.com/problems/binary-tree-level-order-traversal/
这道题要求实现树的层序遍历,其实本质就是把树看成一个有向图,然后进行一次广度优先搜索,这个图遍历算法是非常常见的,这里同样是维护一个队列,只是对于每个结点我们知道它的邻接点只有可能是左孩子和右孩子,具体就不仔细介绍了。算法的复杂度是就结点的数量,O(n),空间复杂度是一层的结点数,也是O(n)。代码如下:
public ArrayList<ArrayList<Integer>> levelOrder(TreeNode root) {
    ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
    if(root == null)
        return res;
    LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
    queue.add(root);
    int curNum = 0;
    int lastNum = 1;
    ArrayList<Integer> list = new ArrayList<Integer>();
    while(!queue.isEmpty())
    {
        TreeNode cur = queue.poll();
        lastNum--;
        list.add(cur.val);
        if(cur.left!=null)
        {
            queue.add(cur.left);
            curNum ++;
        }
        if(cur.right!=null)
        {
            queue.add(cur.right);
            curNum++;
        }
        if(lastNum==0)
        {
            lastNum = curNum;
            curNum = 0;
            res.add(list);
            list = new ArrayList<Integer>();
        }
    }
    return res;
}
层序遍历也是树的一种遍历方式,在某些题目中会比较实用,不过这样其实更接近于图的问题了,在有些树的题目中层序遍历提供了更好的方法,所以还是得熟悉哈。这道题还有一个变体Binary Tree Zigzag Level Order Traversal,其实也是进行广度优先搜索,不过因为要求不同,要换一种数据结构来记录层节点,有兴趣可以看看。

2014年4月9日星期三

Pascal's Triangle II -- LeetCode

原题链接: http://oj.leetcode.com/problems/pascals-triangle-ii/
这道题跟Pascal's Triangle很类似,只是这里只需要求出某一行的结果。Pascal's Triangle中因为是求出全部结果,所以我们需要上一行的数据就很自然的可以去取。而这里我们只需要一行数据,就得考虑一下是不是能只用一行的空间来存储结果而不需要额外的来存储上一行呢?这里确实是可以实现的。对于每一行我们知道如果从前往后扫,第i个元素的值等于上一行的res[i]+res[i+1],可以看到数据是往前看的,如果我们只用一行空间,那么需要的数据就会被覆盖掉。所以这里采取的方法是从后往前扫,这样每次需要的数据就是res[i]+res[i-1],我们需要的数据不会被覆盖,因为需要的res[i]只在当前步用,下一步就不需要了。这个技巧在动态规划省空间时也经常使用,主要就是看我们需要的数据是原来的数据还是新的数据来决定我们遍历的方向。时间复杂度还是O(n^2),而空间这里是O(k)来存储结果,仍然不需要额外空间。代码如下:
public ArrayList<Integer> getRow(int rowIndex) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(rowIndex<0)
        return res;
    res.add(1);
    for(int i=1;i<=rowIndex;i++)
    {
        for(int j=res.size()-2;j>=0;j--)
        {
            res.set(j+1,res.get(j)+res.get(j+1));
        }
        res.add(1);
    }
    return res;
}
这道题相比于Pascal's Triangle其实更有意思一些,因为有一个考点就是能否省去额外空间,在面试中出现的可能性大一些,不过总体比较简单,电面中比较合适。

Pascal's Triangle -- LeetCode

原题链接: http://oj.leetcode.com/problems/pascals-triangle/
这道题比较简单,属于基础的数组操作。基本思路是每层保存前一行的指针,然后当前航数据根据上一行来得到,每个元素就是上一行两个相邻元素相加(第一个和最后一个元素是1)。算法时间复杂度应该是O(1+2+3+...+n)=O(n^2),空间上只需要二维数组来存储结果,不需要额外空间。代码如下:
public ArrayList<ArrayList<Integer>> generate(int numRows) {
     ArrayList<ArrayList<Integer>> res = new ArrayList<ArrayList<Integer>>();
     if(numRows<=0)
        return res;
     ArrayList<Integer> pre = new ArrayList<Integer>();
     pre.add(1);
     res.add(pre);
     for(int i=2;i<=numRows;i++)
     {
         ArrayList<Integer> cur = new ArrayList<Integer>();
         cur.add(1);
         for(int j=0;j<pre.size()-1;j++)
         {
             cur.add(pre.get(j)+pre.get(j+1));
         }
         cur.add(1);
         res.add(cur);
         pre = cur;
     }
     return res;
}
这道题因为是求解每一行结果,所以空间上没什么好讲究的,Pascal's Triangle II只求解某一行的数据,反而可以在空间上进行精简,有兴趣的朋友可以看看哈。

2014年4月8日星期二

Best Time to Buy and Sell Stock III -- LeetCode

原题链接: http://oj.leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/
这道题是Best Time to Buy and Sell Stock的扩展,现在我们最多可以进行两次交易。我们仍然使用动态规划来完成,事实上可以解决非常通用的情况,也就是最多进行k次交易的情况。
这里我们先解释最多可以进行k次交易的算法,然后最多进行两次我们只需要把k取成2即可。我们还是使用“局部最优和全局最优解法”。我们维护两种量,一个是当前到达第i天可以最多进行j次交易,最好的利润是多少(global[i][j]),另一个是当前到达第i天,最多可进行j次交易,并且最后一次交易在当天卖出的最好的利润是多少(local[i][j])。下面我们来看递推式,全局的比较简单,
global[i][j]=max(local[i][j],global[i-1][j]),
也就是去当前局部最好的,和过往全局最好的中大的那个(因为最后一次交易如果包含当前天一定在局部最好的里面,否则一定在过往全局最优的里面)。对于局部变量的维护,递推式是
local[i][j]=max(global[i-1][j-1]+max(diff,0),local[i-1][j]+diff),
也就是看两个量,第一个是全局到i-1天进行j-1次交易,然后加上今天的交易,如果今天是赚钱的话(也就是前面只要j-1次交易,最后一次交易取当前天),第二个量则是取local第i-1天j次交易,然后加上今天的差值(这里因为local[i-1][j]比如包含第i-1天卖出的交易,所以现在变成第i天卖出,并不会增加交易次数,而且这里无论diff是不是大于0都一定要加上,因为否则就不满足local[i][j]必须在最后一天卖出的条件了)。
上面的算法中对于天数需要一次扫描,而每次要对交易次数进行递推式求解,所以时间复杂度是O(n*k),如果是最多进行两次交易,那么复杂度还是O(n)。空间上只需要维护当天数据皆可以,所以是O(k),当k=2,则是O(1)。代码如下:
public int maxProfit(int[] prices) {
    if(prices==null || prices.length==0)
        return 0;
    int[] local = new int[3];
    int[] global = new int[3];
    for(int i=0;i<prices.length-1;i++)
    {
        int diff = prices[i+1]-prices[i];
        for(int j=2;j>=1;j--)
        {
            local[j] = Math.max(global[j-1]+(diff>0?diff:0), local[j]+diff);
            global[j] = Math.max(local[j],global[j]);
        }
    }
    return global[2];
}
可以看到,这里的模型是比较复杂的,主要是在递推式中,local和global是交替求解的。不过理清思路之后,代码是非常简练的,不禁感叹算法真是牛逼哈,这么个复杂生活问题几行代码就解决了。

Triangle -- LeetCode

原题链接: http://oj.leetcode.com/problems/triangle/
这是一道动态规划的题目,求一个三角形二维数组从顶到低端的最小路径和。思路是维护到某一个元素的最小路径和,那么在某一个元素i,j的最小路径和就是它上层对应的相邻两个元素的最小路径和加上自己的值,递推式是sum[i][j]=min(sum[i-1][j-1],sum[i-1][j])+triangle[i][j]。最后扫描一遍最后一层的路径和,取出最小的即可。每个元素需要维护一次,总共有1+2+...+n=n*(n+1)/2个元素,所以时间复杂度是O(n^2)。而空间上每次只需维护一层即可(因为当前层只用到上一层的元素),所以空间复杂度是O(n)。代码如下:
public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
    if(triangle == null || triangle.size() == 0)
        return 0;
    if(triangle.size()==1)
        return triangle.get(0).get(0);
    int[] sums = new int[triangle.size()];
    sums[0] = triangle.get(0).get(0);
    for(int i=1;i<triangle.size();i++)
    {
        sums[i] = sums[i-1]+triangle.get(i).get(i);
        for(int j=i-1;j>=1;j--)
        {
            sums[j] = (sums[j]<sums[j-1]?sums[j]:sums[j-1]) + triangle.get(i).get(j);
        }
        sums[0] = sums[0]+triangle.get(i).get(0);
        
    }
    int minimum = sums[0];
    for(int i=1;i<sums.length;i++)
    {
        if(sums[i]<minimum)
        {
            minimum = sums[i];
        }
    }
    return minimum;
}
上述代码实现时要注意每层第一个和最后一个元素特殊处理一下。

换个角度考虑一下,如果这道题不自顶向下进行动态规划,而是放过来自底向上来规划,递归式只是变成下一层对应的相邻两个元素的最小路径和加上自己的值,原理和上面的方法是相同的,这样考虑到优势在于不需要最后对于所有路径找最小的,而且第一个元素和最后元素也不需要单独处理了,所以代码简洁了很多。代码如下:
public int minimumTotal(List<List<Integer>> triangle) {
    if(triangle.size() == 0)
        return 0;
    int[] res = new int[triangle.size()];
    for(int i=0;i<triangle.size();i++)
    {
        res[i] = triangle.get(triangle.size()-1).get(i);
    }
    for(int i=triangle.size()-2;i>=0;i--)
    {
        for(int j=0;j<=i;j++)
        {
            res[j] = Math.min(res[j],res[j+1])+triangle.get(i).get(j); 
        }
    }
    return res[0];
}
这道题是不错的动态规划题目,模型简单,比较适合在电面中出现。

2014年4月7日星期一

Best Time to Buy and Sell Stock II -- LeetCode

原题链接: http://oj.leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/
这道题跟Best Time to Buy and Sell Stock类似,求最大利润。区别是这里可以交易无限多次(当然我们知道交易不会超过n-1次,也就是每天都进行先卖然后买)。既然交易次数没有限定,可以看出我们只要对于每次两天差价大于0的都进行交易,就可以得到最大利润。因此算法其实就是累加所有大于0的差价既可以了,非常简单。如此只需要一次扫描就可以了,时间复杂度是O(n),空间上只需要O(1)存一个累加结果即可。代码如下:
public int maxProfit(int[] prices) {

    if(prices == null || prices.length==0)
        return 0;
    int res = 0;
    for(int i=0;i<prices.length-1;i++)
    {
        int diff = prices[i+1]-prices[i];
        if(diff>0)
            res += diff;
    }
    return res;
}
这道题其实比Best Time to Buy and Sell Stock更加简单,只需要看透背后的模型就很OK了哈。

Best Time to Buy and Sell Stock -- LeetCode

原题链接: http://oj.leetcode.com/problems/best-time-to-buy-and-sell-stock/
这道题求进行一次交易能得到的最大利润。如果用brute force的解法就是对每组交易都看一下利润,取其中最大的,总用有n*(n-1)/2个可能交易,所以复杂度是O(n^2)。
很容易感觉出来这是动态规划的题目,其实跟Maximum Subarray非常类似,用“局部最优和全局最优解法”。思路是维护两个变量,一个是到目前为止最好的交易,另一个是在当前一天卖出的最佳交易(也就是局部最优)。递推式是local[i+1]=max(local[i]+prices[i+1]-price[i],0), global[i+1]=max(local[i+1],global[i])。这样一次扫描就可以得到结果,时间复杂度是O(n)。而空间只需要两个变量,即O(1)。代码如下:
public int maxProfit(int[] prices) {
    if(prices==null || prices.length==0)
        return 0;
    int local = 0;
    int global = 0;
    for(int i=0;i<prices.length-1;i++)
    {
        local = Math.max(local+prices[i+1]-prices[i],0);
        global = Math.max(local, global);
    }
    return global;
}
这种题目的解法非常经典,不过是比较简单的动态规划。这道题目还有两个变体,Best Time to Buy and Sell Stock IIBest Time to Buy and Sell Stock III,虽然题目要求比较像,但是思路却变化比较大,Best Time to Buy and Sell Stock II可以交易无穷多次,思路还是比较不一样,而Best Time to Buy and Sell Stock III则限定这道题交易两次,其实还可以general到限定交易k次,会在这道题目中仔细讲解,有兴趣的朋友可以看看哈。

2014年4月6日星期日

Symmetric Tree -- LeetCode

原题链接: http://oj.leetcode.com/problems/symmetric-tree/
这道题是树的题目,本质上还是树的遍历。这里无所谓哪种遍历方式,只需要对相应结点进行比较即可。一颗树对称其实就是看左右子树是否对称,一句话就是左同右,右同左,结点是对称的相等。题目中也要求了递归和非递归的解法,关于这个我们已经介绍过很多次了,不了解的朋友可以看看Binary Tree Inorder Traversal,里面介绍了几种树的遍历方式。这道题目也就是里面的程序框架加上对称性质的判断即可。遍历这里就不多说了,我们主要说说结束条件,假设到了某一结点,不对称的条件有以下三个:(1)左边为空而右边不为空;(2)左边不为空而右边为空;(3)左边值不等于右边值。根据这几个条件在遍历时进行判断即可。算法的时间复杂度是树的遍历O(n),空间复杂度同样与树遍历相同是O(logn)。递归方法的代码如下:
public boolean isSymmetric(TreeNode root) {
    if(root == null)
        return true;
    return helper(root.left, root.right);
}
public boolean helper(TreeNode root1, TreeNode root2)
{
    if(root1 == null && root2 == null)
        return true;
    if(root1 == null || root2 == null)
        return false;
    if(root1.val != root2.val)
        return false;
    return helper(root1.left,root2.right) && helper(root1.right,root2.left);
}
下面的非递归方法是使用层序遍历来判断对称性质,代码如下:
public boolean isSymmetric(TreeNode root) {
    if(root == null)
        return true;
    if(root.left == null && root.right == null)
        return true;
    if(root.left == null || root.right == null)
        return false;
    LinkedList<TreeNode> q1 = new LinkedList<TreeNode>();
    LinkedList<TreeNode> q2 = new LinkedList<TreeNode>();
    q1.add(root.left);
    q2.add(root.right);
    while(!q1.isEmpty() && !q2.isEmpty())
    {
        TreeNode n1 = q1.poll();
        TreeNode n2 = q2.poll();
        
        if(n1.val != n2.val)
            return false;
        if(n1.left == null && n2.right != null || n1.left != null && n2.right == null)
            return false;
        if(n1.right == null && n2.left != null || n1.right != null && n2.left == null)
            return false;
        if(n1.left != null && n2.right != null)
        {
            q1.add(n1.left);
            q2.add(n2.right);
        }
        if(n1.right != null && n2.left != null)
        {
            q1.add(n1.right);
            q2.add(n2.left);
        }            
    }
    return true;
}
从上面可以看出非递归方法比起递归方法要繁琐一些,因为递归可以根据当前状态(比如两个都为空)直接放回true,而非递归则需要对false的情况一一判断,不能如递归那样简练。

Word Ladder II -- LeetCode

原题链接: http://oj.leetcode.com/problems/word-ladder-ii/
这道题是LeetCode中AC率最低的题目,确实是比较难。一方面是因为对时间有比较严格的要求(容易超时),另一方面是它有很多细节需要实现。思路上和Word Ladder是比较类似的,但是因为是要求出所有路径,仅仅保存路径长度是不够的,而且这里还有更多的问题,那就是为了得到所有路径,不是每个结点访问一次就可以标记为visited了,因为有些访问过的结点也会是别的路径上的结点,所以访问的集合要进行回溯(也就是标记回未访问)。所以时间上不再是一次广度优先搜索的复杂度了,取决于结果路径的数量。同样空间上也是相当高的复杂度,因为我们要保存过程中满足的中间路径到某个数据结构中,以便最后可以获取路径,这里我们维护一个HashMap,把一个结点前驱结点都进行保存。
在LeetCode中用Java实现上述算法非常容易超时。为了提高算法效率,需要注意一下两点:
1)在替换String的某一位的字符时,先转换成char数组再操作;
2)如果按照正常的方法从start找end,然后根据这个来构造路径,代价会比较高,因为保存前驱结点容易,而保存后驱结点则比较困难。所以我们在广度优先搜索时反过来先从end找start,最后再根据生成的前驱结点映射从start往end构造路径,这样算法效率会有明显提高。
代码如下:
class StringWithLevel {
   String str;
   int level;
   public StringWithLevel(String str, int level) {
      this.str = str;
      this.level = level;
   }
}
public ArrayList<ArrayList<String>> findLadders(String start, String end, HashSet<String> dict) {
   ArrayList<ArrayList<String>> res = new ArrayList<ArrayList<String>>();
   HashSet<String> unvisitedSet = new HashSet<String>();
   unvisitedSet.addAll(dict);
   unvisitedSet.add(start);
   unvisitedSet.remove(end);
   Map<String, List<String>> nextMap = new HashMap<String, List<String>>();
   for (String e : unvisitedSet) {
      nextMap.put(e, new ArrayList<String>());
   }
   LinkedList<StringWithLevel> queue = new LinkedList<StringWithLevel>();
   queue.add(new StringWithLevel(end, 0));
   boolean found = false;
   int finalLevel = Integer.MAX_VALUE;
   int curLevel = 0;
   int preLevel = 0;
   HashSet<String> visitedCurLevel = new HashSet<String>();
   while (!queue.isEmpty()) {
      StringWithLevel cur = queue.poll();
      String curStr = cur.str;
      curLevel = cur.level;
      if(found && curLevel > finalLevel) {
         break;
      }
      if (curLevel > preLevel) {
         unvisitedSet.removeAll(visitedCurLevel);
      }
      preLevel = curLevel;
      char[] curStrCharArray = curStr.toCharArray();
      for (int i = 0; i < curStr.length(); ++i) {
         char originalChar = curStrCharArray[i];
         boolean foundCurCycle = false;
         for (char c = 'a'; c <= 'z'; ++c) {
            curStrCharArray[i] = c;
            String newStr = new String(curStrCharArray);
            if(c != originalChar && unvisitedSet.contains(newStr)) {
               nextMap.get(newStr).add(curStr);
               if(newStr.equals(start)) {
                  found = true;
                  finalLevel = curLevel;
                  foundCurCycle = true;
                  break;
               }
               if(visitedCurLevel.add(newStr)) {
                  queue.add(new StringWithLevel(newStr, curLevel + 1));
               }
            }
         }
         if(foundCurCycle) {
            break;
         }
         curStrCharArray[i] = originalChar;
     }
   }
   if(found) {
       ArrayList<String> list = new ArrayList<String>();
       list.add(start);
       getPaths(start, end, list, finalLevel + 1, nextMap, res);
   }
   return res;
}
private void getPaths(String cur, String end, ArrayList<String> list, int level, Map<String, List<String>> nextMap, ArrayList<ArrayList<String>> res) {
   if(cur.equals(end)){
      res.add(new ArrayList<String>(list));
   }
   else if(level > 0){
      List<String> parentsSet = nextMap.get(cur);
      for (String parent : parentsSet) {
         list.add(parent);
         getPaths(parent, end, list, level - 1, nextMap, res);
         list.remove(list.size() - 1);
      }
   }
}
这道题目实现中有很多细节和技巧,个人觉得如果在面试中遇到很难做到完整,而且考核的算法思想也不是很精妙,更多的是繁琐的操作。在面试中时间上花费太大,也不是很合适,大家主要还是了解一下思路哈。

Word Ladder -- LeetCode

原题链接: http://oj.leetcode.com/problems/word-ladder/
这道题看似一个关于字符串操作的题目,其实要解决这个问题得用图的方法。我们先给题目进行图的映射,顶点则是每个字符串,然后两个字符串如果相差一个字符则我们进行连边。接下来看看这个方法的优势,注意到我们的字符集只有小写字母,而且字符串长度固定,假设是L。那么可以注意到每一个字符可以对应的边则有25个(26个小写字母减去自己),那么一个字符串可能存在的边是25*L条。接下来就是检测这些边对应的字符串是否在字典里,就可以得到一个完整的图的结构了。根据题目的要求,等价于求这个图一个顶点到另一个顶点的最短路径,一般我们用广度优先搜索(不熟悉搜索的朋友可以看看Clone Graph)即可。这个算法中最坏情况是把所有长度为L的字符串都看一下,或者把字典中的字符串都看一下,而长度为L的字符串总共有26^L,所以时间复杂度是O(min(26^L, size(dict)),空间上需要存储访问情况,也是O(min(26^L, size(dict))。代码如下:
public int ladderLength(String start, String end, HashSet<String> dict) {
    if(start==null || end==null || start.length()==0 || end.length()==0 || start.length()!=end.length())
        return 0;
    LinkedList<String> queue = new LinkedList<String>();
    HashSet<String> visited = new HashSet<String>();
    int level= 1;
    int lastNum = 1;
    int curNum = 0;
    queue.offer(start);
    visited.add(start);
    while(!queue.isEmpty())
    {
        String cur = queue.poll();
        lastNum--;
        for(int i=0;i<cur.length();i++)
        {
            char[] charCur = cur.toCharArray();
            for(char c='a';c<='z';c++)
            {
                charCur[i] = c;
                String temp = new String(charCur);
                if(temp.equals(end))
                    return level+1;
                if(dict.contains(temp) && !visited.contains(temp))
                {
                    curNum++;
                    queue.offer(temp);
                    visited.add(temp);
                }
            }
        }
        if(lastNum==0)
        {
            lastNum = curNum;
            curNum = 0;
            level++;
        }
    }
    return 0;
}
可以看出代码框架其实就是广度优先搜索的基本代码,就是判断边的时候需要换字符和查字典的操作,对于这些图的搜索等基本算法,还是要熟悉哈。

2014年4月4日星期五

Binary Tree Maximum Path Sum -- LeetCode

原题链接: http://oj.leetcode.com/problems/binary-tree-maximum-path-sum/
这道题是求树的路径和的题目,不过和平常不同的是这里的路径不仅可以从根到某一个结点,而且路径可以从左子树某一个结点,然后到达右子树的结点,就像题目中所说的可以起始和终结于任何结点。在这里树没有被看成有向图,而是被当成无向图来寻找路径。因为这个路径的灵活性,我们需要对递归返回值进行一些调整,而不是通常的返回要求的结果。在这里,函数的返回值定义为以自己为根的一条从根到子结点的最长路径(这里路径就不是当成无向图了,必须往单方向走)。这个返回值是为了提供给它的父结点计算自身的最长路径用,而结点自身的最长路径(也就是可以从左到右那种)则只需计算然后更新即可。这样一来,一个结点自身的最长路径就是它的左子树返回值(如果大于0的话),加上右子树的返回值(如果大于0的话),再加上自己的值。而返回值则是自己的值加上左子树返回值,右子树返回值或者0(注意这里是“或者”,而不是“加上”,因为返回值只取一支的路径和)。在过程中求得当前最长路径时比较一下是不是目前最长的,如果是则更新。算法的本质还是一次树的遍历,所以复杂度是O(n)。而空间上仍然是栈大小O(logn)。代码如下:
public int maxPathSum(TreeNode root) {
    if(root==null)
        return 0;
    ArrayList<Integer> res = new ArrayList<Integer>();
    res.add(Integer.MIN_VALUE);
    helper(root,res);
    return res.get(0);
}
private int helper(TreeNode root, ArrayList<Integer> res)
{
    if(root == null)
        return 0;
    int left = helper(root.left, res);
    int right = helper(root.right, res);
    int cur = root.val + (left>0?left:0)+(right>0?right:0);
    if(cur>res.get(0))
        res.set(0,cur);
    return root.val+Math.max(left, Math.max(right,0));
}
树的题目大多是用递归方式,但是根据要求的量还是比较灵活多变的,这道题是比较有难度的,他要用返回值去维护一个中间量,而结果值则通过参数来维护,需要一点技巧。

Longest Consecutive Sequence -- LeetCode

原题链接: http://oj.leetcode.com/problems/longest-consecutive-sequence/
这道题是要求出最长的整数连续串。我们先说说简单直接的思路,就是先排序,然后做一次扫描,记录当前连续串长度,如果连续串中断,则比较是否为当前最长连续串,并且把当前串长度置0。这样时间复杂度是很明确,就是排序的复杂度加上一次线性扫描。如果不用特殊的线性排序算法,复杂度就是O(nlogn)。
其实这个题看起来是数字处理,排序的问题,但是如果要达到好的时间复杂度,还得从图的角度来考虑。思路是把这些数字看成图的顶点,而边就是他相邻的数字,然后进行深度优先搜索。通俗一点说就是先把数字放到一个集合中,拿到一个数字,就往其两边搜索,得到包含这个数字的最长串,并且把用过的数字从集合中移除(因为连续的关系,一个数字不会出现在两个串中)。最后比较当前串是不是比当前最大串要长,是则更新。如此继续直到集合为空。如果我们用HashSet来存储数字,则可以认为访问时间是常量的,那么算法需要一次扫描来建立集合,第二次扫描来找出最长串,所以复杂度是O(2*n)=O(n),空间复杂度是集合的大小,即O(n)。代码如下:
public int longestConsecutive(int[] num) {
    if(num == null || num.length == 0)
        return 0;
    HashSet<Integer> set = new HashSet<Integer>();
    int res = 1;
    for(int i=0;i<num.length;i++)
    {
        set.add(num[i]);
    }
    while(!set.isEmpty())
    {
        Iterator iter = set.iterator();
        int item = (Integer)iter.next();
        set.remove(item);
        int len = 1;
        int i = item-1;
        while(set.contains(i))
        {
            set.remove(i--);
            len++;
        }
        i = item+1;
        while(set.contains(i))
        {
            set.remove(i++);
            len++;
        }
        if(len>res)
            res = len;
    }
    return res;
}
这是一个非常不错的题目,有比较好的算法思想,看起来是一个排序扫描的题目,其实想要优化得借助图的算法,模型也比较简单,很适合在面试中提问。

2014年4月3日星期四

Sum Root to Leaf Numbers -- LeetCode

原题链接: http://oj.leetcode.com/problems/sum-root-to-leaf-numbers/
这是一道树的题目,一般使用递归来做,主要就是考虑递归条件和结束条件。这道题思路还是比较明确的,目标是把从根到叶子节点的所有路径得到的整数都累加起来,递归条件即是把当前的sum乘以10并且加上当前节点传入下一函数,进行递归,最终把左右子树的总和相加。结束条件的话就是如果一个节点是叶子,那么我们应该累加到结果总和中,如果节点到了空节点,则不是叶子节点,不需要加入到结果中,直接返回0即可。算法的本质是一次先序遍历,所以时间是O(n),空间是栈大小,O(logn)。代码如下:
public int sumNumbers(TreeNode root) {
    return helper(root,0);
}
private int helper(TreeNode root, int sum)
{
    if(root == null)
        return 0;
    if(root.left==null && root.right==null)
        return sum*10+root.val;
    return helper(root.left,sum*10+root.val)+helper(root.right,sum*10+root.val);
}
树的题目在LeetCode中还是有比较大的比例的,不过除了基本的递归和非递归的遍历之外,其他大部分题目都是用递归方式来求解特定量,大家还是得熟悉哈。