【好书推荐】《剑指Offer》之硬技能(编程题7~11)

本文例子完整源码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

《【好书推荐】《剑指Offer》之软技能》

《【好书推荐】《剑指Offer》之硬技能(编程题1~6)》

持续更新,敬请关注公众号:coderbuff,回复关键字“sword”获取相关电子书。

7.重建二叉树

题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。例如:输入前序遍历序列{1, 2, 4, 7, 3, 5, 6, 8}和中序遍历序列{4, 7, 2, 1, 5, 3, 8, 6}。

  定义二叉树节点

 1 /**
 2 * 二叉树节点
 3 * @author OKevin
 4 * @date 2019/5/30
 5 **/
 6 public class Node<T> {
 7    /**
 8     * 左孩子
 9     */
10    private Node left;
11
12    /**
13     * 右孩子
14     */
15    private Node right;
16
17    /**
18     * 值域
19     */
20    private T data;
21
22    public Node() {
23    }
24
25    public Node(T data) {
26        this.data = data;
27    }
28
29    //省略getter/setter方法
30 }

  解法一:递归

 1 /**
 2 * 根据前序遍历序列和中序遍历序列重建二叉树
 3 * @author OKevin
 4 * @date 2019/5/30
 5 **/
 6 public class Solution {
 7    public Node<Integer> buildBinaryTree(Integer[] preorder, Integer[] inorder) {
 8        if (preorder.length == 0 || inorder.length == 0) {
 9            return null;
10        }
11        Node<Integer> root = new Node<>(preorder[0]);
12        int index = search(0, inorder, root.getData());
13        root.setLeft(buildBinaryTree(Arrays.copyOfRange(preorder, 1, index + 1), Arrays.copyOfRange(inorder, 0, index)));
14        root.setRight(buildBinaryTree(Arrays.copyOfRange(preorder, index + 1, preorder.length), Arrays.copyOfRange(inorder, index + 1, inorder.length)));
15        return root;
16    }
17
18    /**
19     * 在中序遍历的序列中查询根节点所在的位置
20     * @param start 开始查找的下标
21     * @param inorder 中序遍历序列
22     * @param rootData 根节点值
23     * @return 节点值在中序遍历序列中的下标位置
24     */
25    private int search(int start, Integer[] inorder, Integer rootData) {
26        for (; start < inorder.length; start++) {
27            if (rootData.equals(inorder[start])) {
28                return start;
29            }
30        }
31        return -1;
32    }
33 }

二叉树的遍历一共分为:前序遍历、中序遍历和后序遍历

前序遍历遍历顺序为:根节点->左节点->右节点

中序遍历遍历顺序为:左节点->根节点->右节点

后序遍历遍历顺序为:左节点->右节点->根节点

例如二叉树:

1

/     \

2       3

/       /   \

4    5     6

\        /

7   8

前序遍历结果为:1、2、4、7、3、5、6、8

中序遍历结果为:4、7、2、1、5、3、8、6

后序遍历结果为:7、4、2、5、8、6、3、1

此题给出前序和中序的遍历结果,要求重建二叉树。从前序遍历结果得知,第一个节点一定是根节点。从中序遍历结果可知,根节点左侧一定是其左子树右侧一定是其右子树。

那么可以得到:

第一次:

根据前序遍历结果得知,1为根节点,根据中序遍历结果得知,4、7、2为左子树,5、3、8、6为右子树。

第二次:

根据前序遍历结果得知,2为节点,根据中序遍历,4、7位节点2的左子树,节点2没有右子树。

第三次:

根据前序遍历结果得知,4为节点,根据中序遍历,7为节点4的右子树,节点4没有左子树。

以此类推,根据递归即可构建一颗二叉树。

  测试程序

 1 /**
 2 *          1
 3 *         /  4 *        2   3
 5 *       /   /  6 *      4   5   6
 7 *       \     /
 8 *        7   8
 9 * @author OKevin
10 * @date 2019/5/30
11 **/
12 public class Main {
13    public static void main(String[] args) {
14        Integer[] preorder = new Integer[]{1, 2, 4, 7, 3, 5, 6, 8};
15        Integer[] inorder = new Integer[]{4, 7, 2, 1, 5, 3, 8, 6};
16        Solution solution = new Solution();
17        Node<Integer> node = solution.buildBinaryTree(preorder, inorder);
18    }
19 }

8.二叉树的下一个节点

题目:给定一颗二叉树和其中的一个节点,如何找出中序遍历序列的下一个节点?节点中除了两个分别指向左、右子节点的指针,还有一个指向父节点的指针。

分析:熟悉二叉树中序遍历的特点。查找节点的下一个节点,一共有两种情况:一、节点有右子树,节点的下一个节点即为右子树的最左子节点;二、节点没有右子树,此时又要分为两种情况:1、如果节点位于父节点的左节点,节点的下一个节点即为父节点;2、如果节点位于父节点的右节点,此时向上遍历,找到它是父节点的左节点。

  节点定义

 1 /**
 2 * 二叉树节点定义
 3 * @author OKevin
 4 * @date 2019/6/3
 5 **/
 6 public class Node<T> {
 7    /**
 8     * 值域
 9     */
10    private T data;
11
12    /**
13     * 左节点
14     */
15    private Node<T> left;
16
17    /**
18     * 右节点
19     */
20    private Node<T> right;
21
22    /**
23     * 父节点
24     */
25    private Node<T> parent;
26
27    public Node() {
28    }
29
30    public Node(T data) {
31        this.data = data;
32    }
33    //省略getter/setter方法
34 }

中序遍历情况下,查找二叉树节点的下一个节点

 1 /**
 2 * 中序遍历情况下,查找节点的下一个节点
 3 * @author OKevin
 4 * @date 2019/6/3
 5 **/
 6 public class Solution {
 7    public Node getNextNode(Node<Integer> head) {
 8        if (head == null) {
 9            return null;
10        }
11        Node<Integer> p = head;
12        //第一种情况,节点有右子树。节点右子树的最左子节点即为节点中序遍历的下一个节点
13        if (p.getRight() != null) {
14            p = p.getRight();
15            while (p.getLeft() != null) {
16                p = p.getLeft();
17            }
18            return p;
19        }
20        //第二种情况,节点没有右子树。仍然有两种情况:一、节点位于父节点的左节点,此时父节点即为节点中序遍历的下一个节点;二、节点位于父节点的右节点,此时一直向上查找,直到是它父节点的左节点
21        while (p.getParent() != null) {
22            if (p == p.getParent().getLeft()) {
23                return p.getParent();
24            }
25            p = p.getParent();
26        }
27        return null;
28    }
29 }

  测试程序

 1 /**
 2 *          1
 3 *         /  4 *        2   3
 5 *       /   /  6 *      4   5  6
 7 *       \    /
 8 *        7  8
 9 * 中序遍历序列:4,7,2,1,5,3,8,6
10 * @author OKevin
11 * @date 2019/6/3
12 **/
13 public class Main {
14    public static void main(String[] args) {
15        Node<Integer> node1 = new Node<>(1);
16        Node<Integer> node2 = new Node<>(2);
17        Node<Integer> node3 = new Node<>(3);
18        Node<Integer> node4 = new Node<>(4);
19        Node<Integer> node5 = new Node<>(5);
20        Node<Integer> node6 = new Node<>(6);
21        Node<Integer> node7 = new Node<>(7);
22        Node<Integer> node8 = new Node<>(8);
23        node1.setLeft(node2);
24        node1.setRight(node3);
25        node2.setLeft(node4);
26        node2.setParent(node1);
27        node3.setLeft(node5);
28        node3.setRight(node6);
29        node3.setParent(node1);
30        node4.setRight(node7);
31        node4.setParent(node2);
32        node5.setParent(node3);
33        node6.setLeft(node8);
34        node6.setParent(node3);
35        node7.setParent(node4);
36        node8.setParent(node6);
37        Solution solution = new Solution();
38        System.out.println(solution.getNextNode(node6).getData());
39    }
40 }

9.用两个栈实现队列

题目:用两个栈实现一个队列。

分析:栈的结构是FILO(先进后出),队列的结构是FIFO(先进先出)。栈s1用于存储元素,栈s2当执行删除队列尾元素时,从s1弹出数据进入s2,再弹出s2,即实现一个队列。

 1 /**
 2 * 两个栈实现一个队列
 3 * @author OKevin
 4 * @date 2019/6/3
 5 **/
 6 public class MyQueue<T> {
 7    private Stack<T> s1 = new Stack<>();
 8    private Stack<T> s2 = new Stack<>();
 9
10    /**
11     * 从队尾添加元素
12     * @param t 元素
13     * @return 添加的数据
14     */
15    public T appendTail(T t) {
16        s1.push(t);
17        return t;
18    }
19
20    /**
21     * 对队头删除元素
22     * @return 删除的元素
23     */
24    public T deleteTail() {
25        if (s1.empty() && s2.empty()) {
26            return null;
27        }
28        if (s2.empty()) {
29            while (!s1.empty()) {
30                s2.push(s1.pop());
31            }
32        }
33        T t = s2.pop();
34        return t;
35    }
36 }

10.斐波那契数列

题目:求斐波那契数列的第n项。

  解法一:递归

 1 /**
 2 * 求斐波那契数列的第n项
 3 * @author OKevin
 4 * @date 2019/6/3
 5 **/
 6 public class Solution1 {
 7
 8    public Integer fibonacci(Integer n) {
 9        if (n.equals(0)) {
10            return 0;
11        }
12        if (n.equals(1)) {
13            return 1;
14        }
15        return fibonacci(n - 1) + fibonacci(n - 2);
16    }
17 }

优点:简单易懂

缺点:如果递归调用太深,容易导致栈溢出。并且节点有重复计算,导致效率不高。

  解法二:循环

 1 /**
 2 * 求斐波那契数列的第n项
 3 * @author OKevin
 4 * @date 2019/6/3
 5 **/
 6 public class Solution2 {
 7
 8    public Integer fibonacci(Integer n) {
 9        if (n.equals(0)) {
10            return 0;
11        }
12        if (n.equals(1)) {
13            return 1;
14        }
15        Integer first = 0;
16        Integer second = 1;
17        Integer result = first + second;
18        for (int i = 2; i <= n; i++) {
19            result = first + second;
20            first = second;
21            second = result;
22        }
23        return result;
24    }
25 }

通过循环计算斐波那契数列能避免重复计算,且不存在调用栈过深的问题。

11. 旋转数组的最小数字

题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增的数组的一个旋转,输出旋转数组的最小元素。例如,数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

*题中本意是希望能找到数组中的最小数字,直接暴力解法遍历即可。

 引子:通过“二分查找”算法查找有序数组中的数字。

  二分查找有序数组是否存在数字

 1 /**
 2 * 二分查找有序数组中的数字
 3 * @author OKevin
 4 * @date 2019/6/3
 5 **/
 6 public class BinarySearch {
 7
 8    public boolean find(Integer[] array, Integer target) {
 9        Integer start = 0;
10        Integer end = array.length - 1;
11        return partition(array, start, end, target);
12    }
13
14    private boolean partition(Integer[] array, Integer start, Integer end, Integer target) {
15        if (target < array[start] || target > array[end] || start > end) {
16            return false;
17        }
18
19        int middle = (end + start) / 2;
20
21        if (target > array[middle]) {
22            return partition(array, middle + 1, end, target);
23        } else if (target < array[middle]) {
24            return partition(array, start, middle - 1, target);
25        } else {
26            return true;
27        }
28    }
29 }

利用二分法思想查找旋转数组中的最小数字,注意当出现原始数组为:{0,1,1,1,1}时,{1,1,1,0,1}和{1,0,1,1,1}均是旋转数组,这两种情况left=middle=right都是1,不能区别,此时只能按照顺序查找的方式。

 1 /**
 2 * 找到旋转数组中的最小值
 3 * @author OKevin
 4 * @date 2019/6/3
 5 **/
 6 public class Solution {
 7
 8    public Integer find(Integer[] array) {
 9        if (array.length == 0) {
10            return -1;
11        }
12        int left = 0;
13        int right = array.length - 1;
14        int middle = 0;
15        while (array[left] >= array[right]) {
16            if (right - left == 1) {
17                middle = right;
18                break;
19            }
20            middle = (left + right) / 2;
21            if (array[left].equals(array[right]) && array[left].equals(array[middle])) {
22                return min(array, left, right);
23            }
24            if (array[middle] >= array[left]) {
25                left = middle;
26            } else {
27                right = middle;
28            }
29        }
30        return array[middle];
31    }
32
33    /**
34     * 当出现原始数组为:{0,1,1,1,1}时,{1,1,1,0,1}和{1,0,1,1,1}均是旋转数组,这两种情况left=middle=right都是1,不能区别
35     * @param array 数组
36     * @param left 起始
37     * @param right 结束
38     * @return
39     */
40    private Integer min(Integer[] array, int left, int right) {
41        int min = array[left];
42        for (int i = left + 1; i < right; i++) {
43            if (array[i] < min) {
44                min = array[i];
45            }
46        }
47        return min;
48    }
49 }

本文例子完整源码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

《【好书推荐】《剑指Offer》之软技能》

《【好书推荐】《剑指Offer》之硬技能(编程题1~6)》

持续更新,敬请关注公众号:coderbuff,回复关键字“sword”获取相关电子书。

这是一个能给程序员加buff的公众号 (CoderBuff)

原文地址:https://www.cnblogs.com/yulinfeng/p/11001305.html

时间: 2024-11-10 12:52:10

【好书推荐】《剑指Offer》之硬技能(编程题7~11)的相关文章

剑指offer(41-45)编程题

41.入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的. 1 class Solution { 2 public: 3 vector<int> FindNumbersWithSum(vector<int> array,int sum) { 4 int n = array.size(); 5 int left = 0; 6 int right = n-1; 7 while(left < right){

剑指offer(36-40)编程题

两个链表的第一个公共结点 数字在排序数组中出现的次数 二叉树的深度 平衡二叉树 数组中只出现一次的数字 36.输入两个链表,找出它们的第一个公共结点. 1 class Solution1 { 2 public: 3 ListNode* FindFirstCommonNode(ListNode* pHead1, ListNode* pHead2) { 4 ListNode* p = pHead1; 5 ListNode* q = pHead2; 6 unordered_set<ListNode*>

《剑指offer》第六十题:n个骰子的点数

// 面试题60:n个骰子的点数 // 题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s.输入n,打印出s // 的所有可能的值出现的概率. #include <cstdio> #include <math.h> int g_maxValue = 6; // ====================方法一==================== void Probability(int current, int sum, int* pProbabilities); void

剑指Offer之赋值运算符重载(题1)

1 #include<stdio.h>                                                                                                                            2 #include<assert.h>   3    4 class MyString   5 {   6 public:   7     MyString()   8     {   9     

剑指offer——python【第4题】重建二叉树

题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回. 解题思路 用递归思想 前序遍历是{1,2,4,7,3,5,6,8},中序遍历是{4,7,2,1,5,3,8,6},可以知道1是根节点,则在中序遍历中,4,7,2都是1的左子树,5,3,8,6都是1的右子树:从前序遍历又可以知道2是1的左子节点,是1的左

剑指offer——python【第38题】二叉树的深度

题目描述 输入一棵二叉树,求该树的深度.从根结点到叶结点依次经过的结点(含根.叶结点)形成树的一条路径,最长路径的长度为树的深度. 解题思路 想了很久..首先本渣渣就不太理解递归在python中的实现,其次又不知道怎么去找到最长路径,真是很费脑子,开始正题吧 首先明确二叉树每个节点都可以看作"根节点",依次延伸下去(二叉树的递归定义),对于根节点,我要求这个节点的最大深度,那么只要求两棵左右子树的最大深度,并且max一下,然后+1就行了:然后对于左右两棵子树,也只要求它们的两棵左右子树

《剑指offer》第十六题(数值的整数次方)

// 面试题:数值的整数次方 // 题目:实现函数double Power(double base, int exponent),求base的exponent // 次方.不得使用库函数,同时不需要考虑大数问题. #include <iostream> #include <cmath> using namespace std; bool g_InvalidInput = false;//使用全局变量作为错误处理方式 bool equal(double num1, double nu

《剑指offer》第十四题(剪绳子)

// 面试题:剪绳子 // 题目:给你一根长度为n绳子,请把绳子剪成m段(m.n都是整数,n>1并且m≥1). // 每段的绳子的长度记为k[0].k[1].…….k[m].k[0]*k[1]*…*k[m]可能的最大乘 // 积是多少?例如当绳子的长度是8时,我们把它剪成长度分别为2.3.3的三段,此 // 时得到最大的乘积18. #include <iostream> #include <cmath> // ====================动态规划=========

《剑指offer》第二十二题(链表中倒数第k个结点)

// 面试题22:链表中倒数第k个结点 // 题目:输入一个链表,输出该链表中倒数第k个结点.为了符合大多数人的习惯, // 本题从1开始计数,即链表的尾结点是倒数第1个结点.例如一个链表有6个结点, // 从头结点开始它们的值依次是1.2.3.4.5.6.这个链表的倒数第3个结点是 // 值为4的结点. //O(n)的解法:维护两个指针,让A走B前头,前K步,A到头,B就是解了,但是这个问题在于程序鲁棒性 #include <iostream> #include "List.h&q

《剑指offer》第二十四题(反转链表)

// 面试题24:反转链表 // 题目:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的 // 头结点. #include <iostream> #include "List.h" ListNode* ReverseList(ListNode* pHead) { ListNode* pReversedHead = nullptr;//设置三个节点变量,第一是已经被反转的头节点(原链表尾节点) ListNode* pNode = pHead;//第二个是当前节