剑指Offer之每日五道算法题(Java)——第三天

面试题08

问题描述

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

牛客网——二叉树的下一个节点测试用例

实现思路

先总结出所有可能发生的情况,再根据这些情况来写代码,中序遍历下一节点所有情况如下:

  • 节点存在右子树
    • 节点的右孩子存在左子树
      • 则沿着右孩子的左指针一路遍历下去到叶子节点,就是我们要找的值。
    • 节点的右孩子不存在左子树
      • 则节点的右孩子即为我们要找的值。
  • 节点无右子树
    • 节点是根节点
      • 直接返回null
    • 节点不是根节点
      • 节点本身是父节点的左孩子
        • 返回父节点。
      • 节点是父节点的右孩子
        • 沿着父节点指针一路向上遍历,直到找到一个节点,使得该节点是该节点父节点的左孩子,那么该节点的父节点就是我们要找的值。
          (如果找到根节点还没有找到符合条件的节点,则返回null

根据上述情景,来写实现代码:

实现代码

public class Solution {
    static class TreeLinkNode {
        int val;
        TreeLinkNode left = null;
        TreeLinkNode right = null;
        TreeLinkNode next = null;

        TreeLinkNode(int val) {
            this.val = val;
        }
    }

    /**
     * 找出中序遍历指定节点的下一个节点
     * @param pNode 指定节点
     * @return 指定节点的下一个节点
     */
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        /* 判断是否传入的是空二叉树 */
        if(pNode == null) {
            return null;
        }

        /* 判断该节点是否有右子树 */
        if(pNode.right == null) {
            /* 判断是否为根节点 */
            if(pNode.next == null) {
                return null;
            } else {
                /* 判断该节点位于父级节点的左子树还是右子树 */
                if(pNode.next.left == pNode) {
                    return pNode.next;
                } else {
                    /* 根据左根右原则,遍历其父节点,直到找到为父节点左孩子的节点,返回该节点的父节点 */
                    while(pNode.next.next!=null && pNode.next!=pNode.next.next.left) {
                        pNode = pNode.next;
                    }

                    /* 这里不用判空,因为上述如果没有找到合乎条件的节点,pNode.next.next为null退出循环,自然返回null */
                    return pNode.next.next;
                }
            }
        } else {
            /* 若没有左子树则不进入循环,直接返回原节点的右孩子 */
            pNode = pNode.right;
            /* 有左子树,则沿着左孩子遍历下去,直到叶子节点就是下一个值 */
            while(pNode.left != null) {
                pNode = pNode.left;
            }
            return pNode;
        }
    }
}

面试题09

问题描述

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

牛客网——用两个栈实现队列测试用例

实现思路

压入操作就压入到栈A中,以栈A为主栈,而栈B为辅助栈。

因为栈是后进先出,而队列是先进先出,正好和栈相反,所以考虑将栈A的值顺序调换
于是利用栈B,将栈A的值挨个弹出并压入栈B中,则栈B保存的值的顺序正好与栈A相反。
弹出时判断栈B是否为空栈,如果是空栈那么就将栈A的值“倒”入栈B,再弹出栈B位于栈顶的元素。
而如果不是空栈的话,那么直接弹出栈B栈顶的元素即可,因为栈B保存的元素永远是上一次栈A的元素,也就是较早压入的值。

实现代码

public class Solution {
    /**
     * 声明两个栈并初始化
     */
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();

    /**
     * 用两个栈实现队列的压入操作
     * @param node 要压入队列的值
     */
    public void push(int node) {
        stack1.push(node);
    }

    /**
     * 用两个栈实现队列的弹出操作
     * @return 弹出的值
     */
    public int pop() {
        /* 栈B不为空则直接弹出,为空就把栈A的元素“倒”到栈B,再弹出 */
        if(stack2.empty()) {
            while(!stack1.empty()) {
                stack2.push(stack1.pop());
            }
        }

        /* 在空队列删除元素抛出异常 */
        if(stack2.empty()) {
            throw new EmptyStackException();
        }

        return stack2.pop();
    }
}

面试题10

题目一

问题描述

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39

牛客网——斐波那契数列测试用例

实现思路

两种实现思路,第一种是用递归的方法,不断将前两项作为入参相加,直到递归调用到最开始的两项。

但是递归调用有个缺陷,在该题中,有大量的重复操作,可以粗略的看一下当n=10时的递归调用过程:
在这里插入图片描述
可以看到,层级调用的越多,重复现象越严重,这对性能的消耗是非常大的。

而最理想的方法就是用循环从下往上调用,用一个变量来不断存储下一项的值。

实现代码
递归实现
public class Solution {
    public int Fibonacci(int n) {

        /* 基准条件,最开始的两个数为0和1 */
        if(n <= 0) {
            return 0;
        }
        if(n == 1) {
            return 1;
        }

        /* 每次递归使下两项相加 */
        return Fibonacci(n-1) + Fibonacci(n-2);
    }
}
循环实现
public class Solution {
    public int Fibonacci(int n) {
        if(n < 0) {
            throw new IllegalArgumentException("请传入一个非负数。");
        }

        if(n <= 1) {
            return n;
        }

        int fA = 0;
        int fB = 1;
        int fTmp = -1;
        /* 因为一开始已经有2个数了,初始值为0, 所以以 1 为判断条件 */
        while(n != 1) {
            n --;
            
            fTmp = fA + fB;
            fA = fB;
            fB = fTmp;
        }
        
        return fTmp;
    }
}

另外,看牛客网上的大佬们讨论,没有用中间值一样可以做到,循环的代码是这样的:

while(n != 0) {
    n --;

    fB += fA;
    fA = fB - fA;
}
        
return fA;

这个思路主要是利用第三项的特性推导出第二项。
因为下一项永远是fA+fB,所以原来的fB可以利用下一项减去fA推导出来。
这样的话因为第一项和第二项要加两次,所以循环次数要加一次。
(因为第一项是0,所以 0,1 要先变成 1,1 再变成 1,2)

题目二

问题描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

牛客网——跳台阶测试用例

实现思路

每一次选择都包含有两种跳法,一阶或是两阶。
如果第一次跳一阶,那么就剩下n-1阶阶梯,传入f(n-1)让这n-1阶阶梯重复判断跳1还是2
如果第一次跳两阶,那么就剩下n-2阶阶梯,传入f(n-2)让这n-2阶阶梯重复判断跳1还是2
每次选择都包含在上述两者之中,所以最终的数目为 f(n-1)+f(n-2),典型的斐波那契数列。

思路与第一题相同,这里不再细说。

实现代码
递归实现
public class Solution {
    /**
     * 跳台阶递归解法
     * @param target 台阶数目
     * @return 一共多少种方法
     */
    public int JumpFloor(int target) {
        if(target <= 0) {
            throw new IllegalArgumentException("请传入一个正数。");
        }

        /* 基准条件,跳1阶有1种,2阶阶梯有2种 */
        if(target == 1) {
            return 1;
        }
        if(target == 2) {
            return 2;
        }

        return JumpFloor(target-1) + JumpFloor(target-2);
    }
}
循环实现
public class Solution {

    /**
     * 跳台阶循环解法
     * @param target 台阶数目
     * @return 一共多少种方法
     */
    public int JumpFloor(int target) {
        if(target <= 0) {
            throw new IllegalArgumentException("请传入一个正数。");
        }

        if(target <= 2) {
            return target;
        }

        int jA = 1;
        int jB = 2;
        int jTmp = -1;

        /* 因为一开始已经有2个数了,初始从1开始,所以以 2 为判断条件 */
        while(target != 2) {
            target --;

            jTmp = jA + jB;
            jA = jB;
            jB = jTmp;
        }

        return jTmp;
    }
}

面试题11

问题描述

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

牛客网——旋转数组的最小数字测试用例

实现思路

考虑到旋转前数组有序,所以利用二分查找法找最小数字。

具体的做法是:
每次用前指针和中指针指向的数比较。
如果前比中大,则最小值可能在数组后半段,否则可能在数组前半段。
这样不断二分查找,最后前后指针只相差一个数时,找到分界点,后指针所指为最小值
(因为前指针指的一直都是较大的值)

注意特殊情况:
数组中没有元素、数组中只有一个元素、原数组中有若干个重复元素如:{1,2,2,2,2,2}。
当旋转后的数组为{2,2,1,2} 时,无法判断较小值在哪里,只能用顺序查找。

顺序查找的思路是:
不断用前指针和后指针所指的数比较,大就让前指针指向下一个值,直到找到分界点。

实现代码

public class Solution {

    /**
     * 找出旋转数组的最小数字
     * @param array 旋转后的数组
     * @return 数组中的最小数字
     */
    public int minNumberInRotateArray(int [] array) {
        if(array.length == 0) {
            return 0;
        }

        /* 设立两个指针,分别指向前面和后面的数组元素 */
        int start = 0;
        int end = array.length - 1;
        int mid = end / 2;

        /* 旋转了0次,也就是旋转后的数组是非递减情况 */
        if(array[start] < array[end]) {
            return array[start];
        }

        /* 如果三数相等,用顺序查找法,否则用二分查找法 */
        if(array[start]==array[mid] && array[mid]==array[end]) {
            /* 将数组前几个元素跟最后一个比较,直到找到临界点 */
            while(start!=end && array[start]>=array[end]) {
                start ++;
            }

            return array[start];
        } else {
            /* 用二分法查找数组中最小元素,后一个条件是为了排除数组中只有一个元素的情况 */
            while(start!=end-1 && start!=end) {

                /* 把数组一分为二,每次比较数组第一个元素和中间元素的值,小就在后者子数组,大就在前者子数组 */
                if(array[start] <= array[mid]) {
                    start = mid;
                } else {
                    end = mid;
                }

                mid = (end+start) / 2;
            }

            return array[end];
        }
    }
}

面试题23

问题描述

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

牛客网——链表中环的入口节点测试用例

实现思路

有两种实现思路。
第一种是利用哈希表来存储节点,当节点在哈希表中已经存在时,该节点即为环入口节点。
这种方法因为利用了哈希表作为额外存储空间,所以在空间复杂度上不尽如人意。

第二种则是利用两个指针完成

首先判断链表中是否有环
用两个指针,一个每次走两步,一个每次走一步,当他俩相遇的时候,链表有环。
如果走的快的指针走到了null或者其next域为null,链表无环。

其次判断环中节点个数
让指针B先走,同时设置一个计数器,每走一步计数器加一,当再次走到指针A处时,计数器的值就为环中节点个数。

最后判断环的入口节点
重新初始化指针A和B,让指针B先走“环中节点个数”个节点,之后他俩同时开始移动。
这样指针B和指针A再次相遇即为环的入口节点。
其实原因就是用指针B去找链表的结尾,先走环的节点个数,那么他和指针A就始终相差一个环的距离,此时当他走到链表的结尾时,因为他们相差一个环的距离,所以指针A应该正好站在环的门口前。

实现代码

哈希表实现
import java.util.HashMap;
public class Solution {

    static class ListNode {
        int val;
        ListNode next = null;

        ListNode(int val) {
            this.val = val;
        }
    }

    /**
     * 找出链表中环的入口节点
     * @param pHead 链表
     * @return 环的入口节点(若没有则为null)
     */
    public ListNode EntryNodeOfLoop(ListNode pHead) {

        if(pHead==null || pHead.next==null) {
            return null;
        }

        /* 初始化一个额外的哈希表来储存节点 */
        HashMap<ListNode, Integer> map = new HashMap<>();
        while(pHead != null) {
            /* 如果包含该节点,则返回,该节点就是环入口 */
            if(map.containsKey(pHead)) {
                return pHead;
            } else {
                map.put(pHead, 0);
                pHead = pHead.next;
            }
        }

        return null;
    }
}
双指针实现
public class Solution {

     static class ListNode {
        int val;
        ListNode next = null;

        ListNode(int val) {
            this.val = val;
        }
    }

    /**
     * 找出链表中环的入口节点
     * @param pHead 链表
     * @return 环的入口节点(若没有则为null)
     */
    public ListNode EntryNodeOfLoop(ListNode pHead) {

        if(pHead==null || pHead.next==null) {
            return null;
        }

        /* 每次走一步的链表指针 */
        ListNode pList = pHead;
        /* 每次走两步的链表指针 */
        ListNode nList = pHead;

        /* 确定链表中是否有环 */
        while(nList!=null && nList.next!=null) {
            nList = nList.next.next;
            pList = pList.next;

            /* 链表中有环时退出循环 */
            if(pList == nList) {
                break;
            }
        }

        /* 链表无环的情况 */
        if(pList != nList) {
            return null;
        }

        /* 计算环中有几个节点 */
        nList = nList.next;
        int count = 1;
        while(nList != pList) {
            nList = nList.next;
            count ++;
        }

        /* 让指针B先走"环中节点个数"个节点 */
        pList = pHead;
        nList = pHead;
        while(count != 0) {
            nList = nList.next;
            count --;
        }

        /* 指针AB同时行动,再次相遇的节点即为环的入口节点 */
        while(pList != nList) {
            pList = pList.next;
            nList = nList.next;
        }

        return pList;
    }
}

Github

所有面试题的实现我都会放在我的Github仓库中,包括多种实现与详细注释,需要的可以去以下网址查看:
剑指Offer-Java实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值