剑指offer学习笔记(Java实现)(11-20)

来源:互联网 发布:杭州淘宝化妆师招聘 编辑:程序博客网 时间:2024/05/17 23:19

题目11:数值的整数次方
书中方法:这道题要注意底数为0的情况。double类型的相等判断。乘方的递归算法。

    public double power(double base, int exponent){        //指数为0         if(exponent == 0){            return 1.0;        }        //底数为0        if(isEqual(base, 0.0)){            return 0.0;        }        int absExponent = exponent;        if(exponent < 0)absExponent = -absExponent;        double result = unsignedPower(base, absExponent);        if(exponent<0){            result = 1.0/result;        }        return result;    }    private double unsignedPower(double base, int absExponent){        double result = 1.0;        for(int i=1; i<=absExponent; i++){            result *= base;        }        return result;    }    //用递归的方法求乘方    private double unsignedPower2(double base, int absExponent){        if(absExponent == 1)return base;        double result = unsignedPower2(base, absExponent>>1);        result *= result;        if((absExponent & 0x01) == 1){            result *= base;        }        return result;    }    //double类型判断相等    private boolean isEqual(double num1, double num2){        if((num1 - num2)>-0.0000001 && (num1 - num2)<0.0000001){            return true;        }else{            return false;        }    }

题目12:打印1 到 最大的n位数。

书中方法:这道题的一个陷阱在于不能用int或者long去存储你要打印的数,然后用打印函数打印,因为这个数可能会很大。如果加1后超出了最大的n位数,就不打印了。用最高位是否进位判断是否结束,打印的时候注意不要打印出前面可能出现的0.

    public void print(int n){        if(n<=0){            return;        }        //必须要用字符数组防止大数        char[] c = new char[n];        for(int i=0; i<n; i++){            c[i] = '0';        }        while(!increment(c)){            digitsPrint(c);        }    }    private boolean increment(char[] c){        boolean overflow = false;        //因为是加1,当作从小数进位了1.        int carry = 1;        //从个位开始计算        for(int i=c.length-1; i>=0; i--){            int sum = c[i] - '0' + carry;            //如果进位了            if(sum == 10){                if(i == 0){                    overflow = true;                    break;                }                carry = 1;                c[i] = '0';            }else{//如果没进位                c[i] = (char)('0' + sum);                break;            }        }        return overflow;    }    private void digitsPrint(char[] c){        int index = 0;        while(c[index] == '0'){            index++;        }        for(int i=index; i<c.length; i++){            System.out.print(c[i]);        }        System.out.println();    }

我的方法:看到“所有”和“打印”这样的关键字,很容易想到用回溯的方法去做,因为实质是求出所有组合,上面的方法也是为了能遍历到所有组合。和求字符串的全排列以及组合不同(题目28),这里字符可以重复使用(又联想到了打印n位以内数字不重复整数…)。回想一下我们打印字符串的全排列时,因为每个字符只能使用一次,所以我们得想办法保证每个字符只被选取一次(利用额外的数组或者交换)。现在我们只需要简单地在每个位子上选取可能出现的值,然后递归下去就行了。外层是一个控制长度的循环,内层为每一位选取数字,每一位上都有‘0’-‘9’字符可以选择(第一位除外)。

    public void print2(int n){        if(n <= 0)return;        List<String> result = new ArrayList<String>();        String line = "";        //用n控制位数        for(int i=1; i<=n; i++){            find(result, line, 0, i);        }        for(String s : result){            System.out.println(s);        }    }    private void find(List<String> result, String line, int level, int border){        //每一位添加完毕后保存        if(level >= border){            result.add(new String(line));            return;        }        String temp = new String(line);        for(int i=0; i<=9; i++){            //第一位不能为0            if(level == 0 && i == 0){                continue;            }else{                line += i;                find(result, line, level+1, border);                line = temp;            }        }    }

题目13:在O(1)时间删除链表节点。

    public ListNode delete(ListNode head, ListNode toBeDelete){        //如果头节点为空或者只有一个节点        if(head == null || head.next == null)return null;        //如果要删除的节点在末尾        if(toBeDelete.next == null){            ListNode index = head;            while(index.next!= toBeDelete){                index = index.next;            }            index.next = index.next;        }else{//要删除的节点不在末尾            toBeDelete.val = toBeDelete.next.val;            toBeDelete.next = toBeDelete.next.next;        }        return head;    }

题目14:输入整数数组,使所有奇数位于前半部分,所有偶数位于后半部分。

我的方法:想到用两个下标分别表示奇数和偶数的界线,一个在开头,一个在末尾,判断每一个数字的类别,然后将它放入对应的范围内,移动下标,直至两个下标相遇。两个下标,第一个index表示当前要检测的数字以及其左边的数字为奇数(所以当前要检测的数字为奇数的时候,index才会向右移动,否则是even向左移动),even表示其右边的数字为偶数,当index 大于 even的时候分割完毕。这里有个细节,在index表示index左边是奇数,even表示even右边是偶数的前提下 ,如果将while中的“<=”换成“<”(也就是index和even相遇时终止循环,相遇的这个元素无论偶奇不影响分割)也可以完成分割,但此时不知道最后一个元素到底是奇还是偶,如果while中的条件是“<=”(也就是index == even+1时终止循环),我们就可以知道确切的奇偶分界,这一点也可以利用到快速排序中。
这里写图片描述

public class PartitionInArray {    public void partition(int[] a){        if(a == null || a.length == 0)return;        int index = 0;        int even = a.length - 1;        while(index <= even){            if((a[index] & 0x01) == 1){                index ++;            }else{                exch(a, index, even);                even--;            }        }    }    private void exch(int[] a, int l, int m){        int temp = a[l];        a[l] = a[m];        a[m] = temp;    }}

书中的方法:指针odd(odd左边为奇数)指向开头,指针even(even右边为偶数)指向末尾。同时移动,直到两指针“相遇”(书中是相遇就结束,这里我依旧用odd超过even结束)。

    public void partition2(int[] a){        if(a == null || a.length == 0)return;        int odd = 0;        int even = a.length-1;        while(true){            while(odd <= even && isOdd(a[odd])){                odd++;            }            while(odd <= even && !isOdd(a[even])){                even--;            }            if(odd - even == 1){                break;            }            exch(a, odd, even);        }    }    private boolean isOdd(int m){        if((m & 0x01) == 1){            return true;        }        return false;    }

联想:这里很自然地就想到了快速排序的分割——将数组分割成小于目标数字和大于等于目标数字两块,只需要把上述上述中的判断条件改了,同时选取一个用于分割的标准元素即可。

题目15:链表中的倒数第k个节点。

书中方法:用两个节点一次遍历求得倒数第k个节点。注意头节点为空,k<=0,k大于节点个数的情况。

    public ListNode find(ListNode head, int k){        if(head == null || k <=0){            return null;        }        ListNode first = head, second = head;        for(int i=1; i<=k; i++){            //如果k超出了节点的个数            if(first == null){                return null;            }else{                first = first.next;            }        }        while(first != null){            first = first.next;            second = second.next;        }        return second;    }

题目16:输入链表的头节点,反转链表。

书中方法:对于一个链表,我们只能从头往后遍历,如果要反转,我们需要更改当前节点的next域指向前一个节点,此时链表断开,为了能继续修改下一个节点的next域,我们还要维护下一个节点。

    public ListNode reverse(ListNode first){        if(first == null)return first;        ListNode last = null;        ListNode now = first;        ListNode next = first.next;        while(now != null){            now.next = last;            last = now;             now = next;            if(now != null){                next = now.next;            }        }        return last;    }

方法二:书后面还提到了递归的方法。自己写的逻辑比较不清楚,在网上找了一个版本。这个方法的思路是:递归返回的是当前节点右侧已经反转好的链表的头节点,对于当前的节点,将它连接到已经reverse好的链表的末尾,返回值是添加了该节点的新链表头。先递归后处理,最后的返回值是反转后的节点,每次递归返回的是添加了当前节点的反转后的链表。注意递归出口的处理。

    public ListNode reverse2(ListNode first){        if(first == null || first.next == null)return first;        ListNode newHead = reverse2(first.next);        first.next.next = first;        first.next = null;        return newHead;    }

题目17:合并两个排序链表。

我的方法:新初始化一个链表头,比较两个链表当前节点的大小,然后连接到该链表中。遍历两个链表直到null为止。

    public ListNode merge(ListNode first, ListNode second){        //注意这个细节        ListNode head = new ListNode(0);        ListNode index = head;        while(first != null && second != null){            if(first.val < second.val){                index.next = first;                first = first.next;            }else{                index.next = second;                second = second.next;            }            index = index.next;        }        if(first != null)index.next = first;        else index.next = second;        return head.next;    }

书中方法:我们每一次都是找两个链表值中较小的作为结果节点,它的下一个节点依旧会重复一样的过程,这是典型的递归过程,可以得到以下的递归方法。先处理后递归,最后的返回值是头节点,每次递归的返回值是较小的节点。

    public ListNode merge2(ListNode first, ListNode second){        if(first == null)return second;        if(second == null)return first;        ListNode nowHead = null;        if(first.val < second.val){            nowHead = first;            nowHead.next = merge2(first.next, second);        }else{            nowHead = second;            nowHead.next = merge2(first, second.next);        }        return nowHead;    }

题目18:输入两棵二叉树A和B,判断B是不是A的子结构。(补充下,根据书中的代码来看,子结构的定义并不包括叶子节点下的null,也就是说只要B的存在数字的结构存在于A中就行,那么如果B是null树,那么就不属于A的子结构)

书中方法:书上的方法十分清晰,分为两个步骤,先在A树中找到B树的root.val,然后判断由该点向下是否完全包含B树的结构,直至遍历完A树。

    public boolean isChild(TreeNode aRoot, TreeNode bRoot){        //A树没有遍历完而且B树不为空        if(aRoot != null && bRoot != null){            //在当前节点检查结构,或者去遍历当前节点的左节点或右节点。            return isQualified(aRoot, bRoot)                     || isChild(aRoot.left, bRoot)                     || isChild(aRoot.right, bRoot);        }        //A树遍历完了或者B树是个null        return false;    }    private boolean isQualified(TreeNode aRoot, TreeNode bRoot){        //检查到了B的末尾        if(bRoot == null)return true;        //如果在检查完B之前A到了底        if(aRoot == null)return false;        //都不是null且val相等,继续检查        if(aRoot.val == bRoot.val){            return isQualified(aRoot.left, bRoot.left)                     && isQualified(aRoot.right, bRoot.right);        }        //都不是null但是val不等        return false;    }

我的方法:开始检查一下B是否为空,空的话返回false(定义)。aRoot和bRoot是两个根节点,且都可能在自己的根节点和子节点之间转换。开头,如果aRoot.val和bRoot.val相等了,那么继续对各自的子节点进行对比;如果不相等则移动aRoot并和bRoot进行比较。

    public boolean isChildTree(TreeNode aRoot, TreeNode bRoot){        //定义,如果B树为空,返回false        if(bRoot == null){            return false;        }        return isChild2(aRoot, bRoot);    }    private boolean isChild2(TreeNode aRoot, TreeNode bRoot){        //如果检查到了B的末尾        if(bRoot == null)return true;        //A到了末尾但是B没有        if(aRoot == null)return false;        //如果当前节点相同,进行进一步对比;        //对比的结果可能是false,此时继续向下检查aRoot.left与bRoot、aRoot.right与bRoot        if(aRoot.val == bRoot.val){            return (isChild2(aRoot.left, bRoot.left) && isChild2(aRoot.right, bRoot.right))                    || isChild2(aRoot.left, bRoot)                    || isChild2(aRoot.right, bRoot);        }else{//如果当前节点不同,继续向下检查aRoot.left与bRoot、aRoot.right与bRoot            return isChild2(aRoot.left, bRoot)                    || isChild2(aRoot.right, bRoot);        }    }

题目19:二叉树的镜像。

书中方法:这道题目可能拿到手没有思路,我们可以在纸上画出简单的二叉树来找到规律。最后我们发现,镜像的实质是对于二叉树的所有节点,交换其左右子节点。搞清楚获得镜像的方法,这道题实际上就变成了一道二叉树遍历的变形。这里选择前序遍历二叉树。

    public void change(TreeNode root){        if(root == null)return;        TreeNode temp = root.left;        root.left = root.right;        root.right = temp;        change(root.left);        change(root.right);    }

如果改成循环实现:实际上也就是把递归改成循环前序遍历二叉树。注意null是可以被压入栈(队列、HashMap等)的,栈全部是null元素和栈为空不同。

    public void change2(TreeNode root){        if(root == null)return;        Stack<TreeNode> stack = new Stack<TreeNode>();        stack.push(root);        while(!stack.isEmpty()){            TreeNode now = stack.pop();            if(now.right != null)stack.push(now.right);            if(now.left != null)stack.push(now.left);            TreeNode temp = now.left;            now.left = now.right;            now.right = temp;        }    }

题目20:顺时针打印矩阵。

我的方法:遇到这种题最好在纸上画一画打印路线。我利用了4个标志left、top、right、bottom,表示当前需要打印的左界、上届、右界和下界,换句话说这些界线之外的已经打印了,如此一来判断结束的标准也很清晰,top>bottom或者left>right就表示已经没有需要打印的空间。和第14题中确定结束条件类似,明确下标的含义,就能很快判断结束条件。

    public void myPrint(int[][] a){        if(a == null || a.length == 0 || (a.length == 1 && a[0].length == 0)){            return;        }        int left = 0;        int right = a[0].length -1;        int top = 0;        int bottom = a.length - 1;        //用于改变方向,分别代表  从左向右打印,从上往下打印,从又往左打印,从下往上打印。        int[] orientations = new int[]{0, 1, 2, 3};        int count = 0;        while(left <= right && top <= bottom){             //按顺序改变方向            switch (orientations[count%orientations.length]) {            case 0:                //从左向右打印一行,打印完后上界下移                for(int i=left; i<=right; i++){                    System.out.print(a[top][i]);                }                top++;                count++;                break;            case 1:                //从上到下打印一列,打印完后右界左移                for(int i=top; i<=bottom; i++){                    System.out.print(a[i][right]);                }                right--;                count++;                break;            case 2:                //从右到左打印一行,打印完后下界上移                for(int i=right; i>=left; i--){                    System.out.print(a[bottom][i]);                }                bottom--;                count++;                break;            case 3:                //从下到上打印一列,打印完后左界右移                for(int i=bottom; i>=top; i--){                    System.out.print(a[i][left]);                }                left++;                count++;                break;            default:                break;            }        }    }

书中方法:书中的思路是一圈一圈向内打印,既然这样就要找到每一圈的起点和打印的圈。假设我们用count表示已经打印的圈数。起点就是(count, count);打印的圈是一个矩形条框,矩形可以由一条对角线确定,我们找到矩形条框右下角的点,就能由这个点和起点确定一个需要打印的圈(矩形框)。只要有起点的空间,就继续打印

    public void myPrint2(int[][] a){        if(a == null || a.length == 0 || (a.length == 1 && a[0].length == 0)){            return;        }        int count = 0;        //打印了count次后,如果还留有下一次打印的起始点的位置,继续打印        while(a.length - 2*count > 0 && a[0].length - 2*count > 0){            printRound(a, count);            count++;        }    }

打印的范围确定了,剩下就是按顺时针打印每一个圈。

    private void printRound(int[][] a, int count){        int endX = a[0].length - 1 - count;        int endY = a.length - 1 - count;        int start = count;        //无论如何第一行都需要打印        for(int i=start; i<=endX; i++){            System.out.print(a[start][i]);        }        //如果不止一行        if(start < endY){            for(int i=start+1; i<=endY; i++){                System.out.print(a[i][endX]);            }        }        if(start < endX && start < endY){            for(int i=endX-1; i>=start; i--){                System.out.print(a[endY][i]);            }        }        if(start < endX && start < endY-1){            for(int i=endY-1; i>start; i--){                System.out.print(a[i][start]);            }        }    }
0 0
原创粉丝点击