标签 剑指offer 下的文章

一、题目描述

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

  • 输入:arr = [3,2,1], k = 2
  • 输出:[1,2] 或者 [2,1]

示例 2:

  • 输入:arr = [0,1,2,1], k = 1
  • 输出:[0]

限制:

  • 0 <= k <= arr.length <= 10000
  • 0 <= arr[i] <= 10000

二、题解

解法一:排序

一个最简单的思路是先对所有元素排序,然后输出前面k个元素。使用快速排序的话,时间复杂度O(nlog(n))。

解法二:时间复杂度为O(n)的快速排序

快速排序的特点是:每次排序后,标杆元素左边的元素都比它小,右边的元素都比它大。因此可以利用这一个特性,对数组中的元素进行部分排序,直到返回的标杆元素下标等于k,那么这个标杆元素的左边就是所有需要的目标数据。

相较于快排来说,只需要对目标区间(标杆元素左边或者右边)进行排序就行了,无需对左右两边的区间都进行排序。因此时间复杂度降为O(n)。

解法三:最大堆

最大堆的性质是:堆上层的元素都大于等于下层元素。因此,只要维护一个大小为k的最大堆,循环遍历数组,如果元素大于堆顶元素,就替换掉堆顶元素,重新建堆。堆里面保存的就是k个最小的元素了。时间复杂度O(nlog(k)),空间复杂度O(k)。

三、代码:

3.1 基于快排思想的算法

class Solution {
private:
    int partition(vector<int> &arr, int left, int right) {
        int i, j, key;

        if (left >= right)
            return left;

        i = left;
        j = right;

        key = arr[i];
        while (i < j) {
            while (i < j && arr[j] >= key) {
                j--;
            }
            if (i < j && arr[j] < key) {
                arr[i] = arr[j];
            }

            while (i < j && arr[i] <= key) {
                i++;
            }
            if (i < j && arr[i] > key) {
                arr[j] = arr[i];
            }
        }
        arr[i] = key;

        return i;
    }

public:
    vector<int> getLeastNumbers(vector<int> &arr, int k) {
        int idx, left, right;
        vector<int> output;

        if (arr.size() <= k)
            return arr;
        if (arr.size() == 0)
            return output;

        left = 0;
        right = arr.size() - 1;
        // 循环分区,找到第k个标杆元素
        idx = partition(arr, left, right);
        while (idx != k) {
            if (idx > k) {
                // 标杆元素的下标大于k,要在左区间继续找
                right = idx - 1;
                idx = partition(arr, left, right);
            } else {
                // 标杆元素的下标小于k,要在右区间继续找
                left = idx + 1;
                idx = partition(arr, left, right);
            }
        }

        // 预分配空间,不要直接push_back,否则会浪费很多重新开辟空间的时间
        output.reserve(k);
        for (idx = 0; idx < k; idx++) {
            output.push_back(arr[idx]);
        }

        return output;
    }
};

3.2 使用最大堆

class Solution {
private:
    // 执行最大堆化
    void maxHeapify(vector<int> &v, int idx) {
        int mid, i, left, right, largest;
        left = (idx * 2) + 1;
        right = left + 1;

        if (left < v.size() && v[left] > v[idx])
            largest = left;
        else
            largest = idx;

        if (right < v.size() && v[right] > v[largest])
            largest = right;

        if (largest != idx) {
            swap(v[largest], v[idx]);
            maxHeapify(v, largest);
        }
    }
public:
    vector<int> getLeastNumbers(vector<int> &arr, int k) {
        int i, j, len;
        vector<int> output;

        if (k >= arr.size())
            return arr;

        if (k == 0)
            return output;

        len = 0;
        output.reserve(k);

        for (i = 0; i < arr.size(); i++) {
            if (len < k) {
                // 当最大堆的元素个数小于k时,直接插入到堆
                output.push_back(arr[i]);
                len++;

                // 堆塞满后,构造最大堆
                if (len == k) {
                    for (j = k / 2; j >= 0; j--) {
                        maxHeapify(output, j);
                    }
                }

            } else {
                // 元素小于堆顶元素,替换掉首元素
                if (arr[i] < output[0]) {
                    output[0] = arr[i];
                    maxHeapify(output, 0);
                }
            }
        }

        return output;
    }
};

【每日打卡】[leetcode+剑指offer] 169-多数元素

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/majority-element

著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这道题也是《剑指offer》原题——面试题39数组中出现次数超过一半的数字。

https://leetcode-cn.com/problems/shu-zu-zhong-chu-xian-ci-shu-chao-guo-yi-ban-de-shu-zi-lcof/

一、题目描述

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

  • 输入: [3,2,3]
  • 输出: 3

示例 2:

  • 输入: [2,2,1,1,1,2,2]
  • 输出: 2

二、题解

2.1 哈希表

使用哈希表的思路:用数字作为key,每遇到一次,值加一。遍历完数字后,找出次数最大的。

思路很简单,时间复杂度O(N),空间复杂度O(N),N表示哈希表槽大小。

2.2 排序

因为元素在数组出现一半以上,所以排序后的中位数一定就是目标元素。

使用快速排序时间复杂度O(Nlog(N)),空间复杂度O(1)。

2.3 摩尔投票法

摩尔投票法的核心思想:因为元素出现次数超过一半,所以它出现的次数减去剩余元素出现次数一定是大于0的。基于这个思想,可以使用抵消的办法来消除掉所有非目标元素,然后剩下的就是目标元素了。

可以先随机找一个元素作为标杆,然后往后遍历,这个标杆元素每出现一次,次数加一。只要出现非标杆元素,次数减一。当次数变成0后,重新设置标杆元素,重复上面的过程。直到最后剩下来的元素就是目标元素。

以target表示目标元素,count表示出现的次数。过程描述:

array |[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
target| 7                | 5    | 5          | 7
count | 1, 2, 1, 2, 1, 0 | 1, 0 | 1, 2, 0, 0 | 1, 2, 3, 4

最后剩下来的目标元素是7,也就是我们需要的值。

时间复杂度O(N),空间复杂度0(1)。

三、代码

using namespace std;

class Solution {
public:
    int majorityElement(vector<int> &nums) {
        int count = 0, x;
        if (nums.empty()) {
            throw length_error("empty array");
        }

        x = nums[0];
        for (auto i : nums) {
            if (count == 0) {
                x = i;
            }
            if (x == i) {
                count++;
            } else {
                count--;
            }
        }

        return x;
    }
};

一、题目描述

请实现函数complex_list_node *clone_list(complex_list_node *head),复制一个复杂链表。在复杂链表中,每个节点除了有一个next指针指向下一个节点,还有一个random指针指向链表中的随机节点或nullptr。节点的C++定义如下:

struct complex_list_node {
    int val;
    struct complex_list_node *next;
    struct complex_list_node *random;
};

如下是一个复杂链表示例,其中虚线是random指针:

二、思路

复制过程可以分为三步,第一步,复制链表中的所有节点并插入到对应节点后,random指针和原节点指针保持一致:

第二步,修改所有复制节点的random指针,修改成被复制出来的节点:

第三步,把所有新复制出来的节点从原始链表中移除:

三、代码实现

链表节点定义:

// 链表节点定义
struct complex_list_node {
    int val;
    struct complex_list_node *next; // 下一个节点
    struct complex_list_node *random; // 随机节点
};

第一步克隆所有节点函数:

/*
 * 克隆所有的节点,并串到原始节点中去
 * @head 链表的头结点
 */
static void clone_all_nodes(complex_list_node *head) {
    complex_list_node *node, *clone;
    node = head;
    while (node) {
        // 拷贝节点
        clone = new complex_list_node{node->val, node->next, node->random};
        // 修改p节点的下一个节点指向新创建的节点
        node->next = clone;
        // 移动p指针到下一个节点
        node = clone->next;
    }
}

第二步修改random指针指向:

/*
 * 修正所有克隆节点的随机指针的地址
 * @head 链表的头结点
 */
static void fix_random_nodes(complex_list_node *head) {
    // node/q节点分别表示原始节点和被克隆的节点
    complex_list_node *node, *clone;
    node = head;
    while (node) {
        clone = node->next;
        // 修正q节点的random指针,注意random可能为null
        if (clone->random)
            clone->random = clone->random->next;
        node = clone->next;
    }
}

第三步分离原始链表和新克隆链表:

/*
 * 修改新复制节点的指向,重新连接所有节点
 * @head 头结点指针
 * @return 新复制的链表头结点
 */
static complex_list_node *reconnect_nodes(complex_list_node *head) {
    complex_list_node *node, *clone;
    complex_list_node *new_head = head->next;

    node = head;
    while (node) {
        clone = node->next;
        // 备份下一个节点的指针
        node->next = clone->next;
        // 连接到新克隆的节点,注意next可能为null
        if (clone->next) {
            clone->next = clone->next->next;
        }
        node = node->next;
    }
    return new_head;
}

整合起来的复杂链表拷贝函数:

complex_list_node *clone_complex_list(complex_list_node *head) {
    if (head == nullptr)
        return nullptr;
    clone_all_nodes(head);
    fix_random_nodes(head);
    return reconnect_nodes(head);
}

注意:这个函数里面一定要判断head是否为空的情况,前面三个函数因为是内部函数所以没有在入口判断head为空的情况!

四、单元测试

写代码容易,写单元测试麻烦,一个单元测试写了一小时。。。

4.1 链表创建函数

做单元测试,必不可少的一个测试函数是链表创建函数,因为要测试链表,首先要先创建链表:

/*
 * 链表创建函数
 * @val 数据数组
 * @random 随机指针
 * @n 数组数量
 * @return 返回链表头
 */
static complex_list_node *list_creator(int data[], int random[], int n) {
    complex_list_node *head, **nodes, *p;
    int i;
    nodes = new p_complex_list_node[n];

    // 创建所有的链表节点
    for (i = 0; i < n; i++) {
        p = new complex_list_node{data[i], nullptr, nullptr};
        nodes[i] = p;
    }

    // 连接所有的next和random指针
    for (i = 0; i < n - 1; i++) {
        nodes[i]->next = nodes[i + 1];
        if (random[i] == -1) {
            nodes[i]->random = nullptr;
        } else {
            nodes[i]->random = nodes[random[i]];
        }
    }
    nodes[i]->random = nodes[random[i]];

    p = nodes[0];
    delete nodes;

    return p;
}

测试这个函数的正确性:

// 测试创建链表的功能是不是OK
TEST(complex_list_node, list_creator) {
    // 数据数组
    int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 随机指针数组
    int random[10] = {2, 3, 6, -1, 3, 1, 0, 9, 5, 3};
    int i;
    complex_list_node *head, *p, *r, *n;
    // 创建链表
    head = list_creator(arr, random, 10);
    p = head;
    for (i = 0; i < 10 && p; i++, p = p->next) {
        r = p->random;
        EXPECT_EQ(p->val, arr[i]);
        if (random[i] == -1) {
            EXPECT_EQ(r, nullptr);
        } else {
            EXPECT_EQ(r->val, arr[random[i]]);
        }
    }
}

4.1 测试功能

测试功能点:

  1. 空链表测试
  2. 非空链表测试
  3. 链表克隆后是否会影响原始链表

leetcode上也有这个一模一样的题目,下面的测试数据是基于leetcode来写的。

https://leetcode-cn.com/problems/copy-list-with-random-pointer/submissions/

TestFixtures定义:

const int leetcode_demo1_cnt = 5;
class leetcode_test : public ::testing::Test {
protected:
    void SetUp() override {
        // 初始化list1
        int arr[leetcode_demo1_cnt] = {7, 13, 11, 10, 1};
        int random[leetcode_demo1_cnt] = {-1, 0, 4, 2, 0};
        list1 = list_creator(arr, random, leetcode_demo1_cnt);
        // list2为空
        list2 = nullptr;
    }

    void TearDown() override {
        complex_list_node *p = list1, *next;
        while (p) {
            next = p->next;
            delete p;
            p = next;
        }
    }

    complex_list_node *list1;
    complex_list_node *list2;
};

测试克隆链表案例:

TEST_F(leetcode_test, clone_nodes) {
    int i;
    complex_list_node *clone, *p, *q;
    clone = clone_complex_list(list1);

    p = list1;
    q = clone;
    // 遍历所有节点,所有节点的内容和原节点一样,并且克隆出来的节点并不是原链表上面的指针
    for (i = 0; i < leetcode_demo1_cnt && p && q; i++, p = p->next, q = q->next) {
        // 两个节点不相等
        EXPECT_NE(p, q);
        // 节点的值都一样
        EXPECT_EQ(p->val, q->val);
        if (p->next) {
            EXPECT_TRUE(q->next);
            EXPECT_NE(p->next, q->next);
        }
        if (p->random) {
            EXPECT_TRUE(q->random);
            EXPECT_NE(q->random, p->random);
        }
    }
    // 都遍历的链表结尾了
    EXPECT_FALSE(p);
    EXPECT_FALSE(q);
    EXPECT_EQ(i, leetcode_demo1_cnt);
}

测试空链表复制:

TEST_F(leetcode_test, empty_list) {
    complex_list_node *clone;
    clone = clone_complex_list(list2);
    EXPECT_FALSE(clone);
}

测试是否修改了原链表:

TEST_F(leetcode_test, origin_list_not_modify) {
    int i = 0;
    complex_list_node *bak[leetcode_demo1_cnt], *p, *clone;
    // 备份原始链表的所有next指针
    p = list1;
    while (p) {
        bak[i++] = p->next;
        p = p->next;
    }

    clone = clone_complex_list(list1);

    // 和克隆前的next指针对比,确认没有影响到原始指针
    p = list1;
    i = 0;
    while (p) {
        EXPECT_EQ(bak[i++], p->next);
        p = p->next;
    }
}

测试结果:

一、题目

输入一个链表,输出该链表中倒数第k个节点,为了符合大多数人的习惯,k的序号从1开始,即链表的尾结点是倒数第一个节点。

例如,如下链表中的倒数第3个节点是3

二、解题思路

使用快慢指针,快指针先走n步,然后快慢指针同时走,直到快指针为null,慢指针就是倒数第n个节点。

以上面的链表为例,初始时,两个指针都指向链表开头:

fast指针先往前走三步:

此时,fast和slow同时往前走,直到fast为空:

此时slow所在的位置刚好就是倒数第三个节点。

三、代码

代码中需要注意的问题:

  1. 链表可能为空,避免空指针访问。
  2. 链表长度可能不够n,遍历fast的时候注意判断非空,并且外部应该判断出这一点。
list_node *find_kth_to_tail(list_node *head, int n) {
    list_node *slow, *fast;
    
    // [1] 注意链表为空的情况
    if (head == nullptr)
        return nullptr;

    fast = head;
    slow = head;

    // [2] fast指针先走n步
    while (fast && n--) {
        fast = fast->next;
    }

    // [3] 整个链表长度不够n
    if (!fast && n > 0)
        return nullptr;

    while (fast) {
        fast = fast->next;
        slow = slow->next;
    }

    return slow;
}

四、单元测试

单元测试主要覆盖以下几点:

  1. 链表为空的场景。
  2. 链表不为空,获取倒数第1个节点。
  3. 链表不为空,获取倒数第n个节点,n等于链表长度。
  4. 链表不为空,获取倒数第n个节点,n大于链表长度。

TestFixtures定义了三个链表,第一个链表有5个元素,第二个链表为空,第三个链表有1个元素。

class list_test : public ::testing::Test {
protected:
    void SetUp() override {
        int i, arr[] = {2, 3, 4, 5};
        // 初始化链表1
        list_node *p;
        p = new list_node{1, nullptr};
        list1 = p;
        for (i = 0; i < 4; i++) {
            p->next = new list_node{arr[i], nullptr};
            p = p->next;
        }
        // 初始化链表2
        list2 = nullptr;
        // 初始化链表3
        list3 = new list_node{1, nullptr};
    }

    // 释放链表节点
    void TearDown() override {
        list_node *p, *next;
        p = list1;
        while (p) {
            next = p->next;
            delete p;
            p = next;
        }
        delete list3;
        list3 = nullptr;
    }

    // 初始化三个链表,第一个链表有5个节点,第二个链表为空,第三个链表只有一个节点
    list_node *list1;
    list_node *list2;
    list_node *list3;
};

测试案例一:获取倒数第n个节点,n分别大于、小于和等于链表长度。

// 案例:获取倒数第n个节点,n小于链表长度
TEST_F(list_test, multi_node) {
    list_node *p;
    // 倒数第一个节点
    p = find_kth_to_tail(list1, 1);
    EXPECT_EQ(p->data, 5);
    // 倒数第二个节点
    p = find_kth_to_tail(list1, 2);
    EXPECT_EQ(p->data, 4);
    // 倒数第三个节点
    p = find_kth_to_tail(list1, 3);
    EXPECT_EQ(p->data, 3);
    // 倒数第四个节点
    p = find_kth_to_tail(list1, 4);
    EXPECT_EQ(p->data, 2);
    // 倒数第五个节点
    p = find_kth_to_tail(list1, 5);
    EXPECT_EQ(p->data, 1);
    // 倒数第六个节点 --> 空
    p = find_kth_to_tail(list1, 6);
    EXPECT_FALSE(p);
}

测试案例二:测试空链表。

// 案例:空链表测试
TEST_F(list_test, no_node) {
    list_node *p;
    p = find_kth_to_tail(list2, 1);
    EXPECT_FALSE(p);
    p = find_kth_to_tail(list2, 2);
    EXPECT_FALSE(p);
}

测试案例三:只有一个节点的链表测试。

// 案例:只有一个节点的链表测试
TEST_F(list_test, one_node) {
    list_node *p;
    // 倒数第一个节点,不为空
    p = find_kth_to_tail(list3, 1);
    EXPECT_EQ(p->data, 1);
    // 倒数第二个节点,空
    p = find_kth_to_tail(list3, 2);
    EXPECT_FALSE(p);
    // 倒数第三个节点,空
    p = find_kth_to_tail(list3, 3);
    EXPECT_FALSE(p);
}

测试结果:

一、题目

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

以下面的二叉树为例,它的中序遍历序列是:[2, 4, 1, 5, 3, 6],节点4的下一个节点是1,节点3的下一个节点是5。

二、分析

画出上面这棵二叉树的中序遍历过程为:

根据中序遍历的特性,可以整理出来的下一个节点的规律:

  1. 如果节点有右子节点,那么下一个节点一定在右子树中,这种又分成了2种情况。

    • 右儿子没有子节点,那么下一个节点就是子节点(节点2
    • 如果右子节点有子节点,下一个节点就是右子节点种最左的节点(节点1)。
  2. 如果节点没有右子节点,并且是父节点的左子节点,那么下一个节点就是父亲节点(节点5)。
  3. 如果节点没有右子节点,并且是父节点的右子节点,那就递归查找父节点,直到一个父节点是祖父节点的左子树(节点4)。如果找到根节点都没有找到,就说明该节点已经是最后一个了(节点6)。

三、代码

类节点定义:

template<typename T>
class tree_node {
private:
    T data;
    tree_node *lchild;
    tree_node *rchild;
    tree_node *parent;
};

查找中序遍历序列中下一个节点的成员函数:

// 获取中序遍历的下一个节点
const tree_node<T> *get_next_node_in_order() const {
    const tree_node<T> *p, *father;
    if (rchild) { // 有右节点,在右子树中查找
        p = rchild;
      
        // 循环遍历左子树
        while (p->lchild)
            p = p->lchild;
      
        return p;
    } else if (parent) {
        p = this;
        father = parent;
      
        // 只要不是父节点的左子节点,一直往上找
        while (father && p != father->lchild) {
            p = father;
            father = father->parent;
        }

        // 返回父亲节点
        return father;
    }
    return nullptr;
}

四、单元测试

单元测试主要覆盖几个点:

  • 节点有右子节点。
  • 节点没有右子节点,存在一个父节点是祖先节点的左子节点。
  • 节点没有右子节点,不存在一个父节点是祖先节点的左子节点。
/*
 * 测试案例:查找二叉树中序遍历的下一个节点
 *         1
 *      /     \
 *     2       3
 *      \     / \
 *       4   5   6
 */
TEST(bin_tree, get_next_node_in_order) {
    bin_tree<int> tree;
    const tree_node<int> *node;
    int in[] = {2, 4, 1, 5, 3, 6}; // 先序遍历序列
    int pre[] = {1, 2, 4, 3, 5, 6}; // 中序遍历序列
    
    // 根据先序遍历序列和中序遍历序列生成二叉树
    tree.construct(pre, in, 6);
    EXPECT_TRUE(tree.get_root());

    // 定位到中序遍历的开始节点2,然后依次获取下一个节点,和in数组对比
    node = tree.get_root()->get_lchild();
    for (int i = 0; i < 6; i++) {
        EXPECT_TRUE(node) << i;
        EXPECT_EQ(node->get_data(), in[i]);
        node = tree.get_next(node);
    }
    EXPECT_EQ(node, nullptr);
}

测试结果:

一、题目

输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不包含重复的数字。

例如,输入前序遍历序列[1, 2, 4, 7, 3, 5, 6, 8]和中序遍历序列[4, 7, 2, 1, 5, 3, 8, 6],则重建如下图所示的二叉树并输出它的头结点:

二、解析

根据前序、中序遍历的特性和上面的图形能得到的结论:

  • 前序遍历的第一个元素就是树的根节点。
  • 根节点左边的元素就是左子树,根右边的就是右子树。

图片描述:

基于这两个特性就可以通过以下方式来构建出一颗二叉树:从前序遍历中确认根节点,再到中序遍历中找到定位到这个根节点,这样就可以拿到对应的左子树和右子树了。然后再按照相同的方式分别得到左右子树的子树,那么整个二叉树就可以构建起来了。

三、代码实现

二叉树节点定义:

template<typename T>
class tree_node {
    friend class bin_tree<T>;
    tree_node(const T &data) : data(data), lchild(nullptr), rchild(nullptr) {}
private:
    T data;
    tree_node *lchild;
    tree_node *rchild;
}

二叉树定义:

class bin_tree {
public:
    bin_tree() : root(nullptr) {}
    tree_node<T> *root;
};

在二叉树类中实现通过前序遍历和中序遍历构造二叉树的函数:

/*
 * 根据前序和中序遍历序列构造一个二叉树
 * @pre_order 前序遍历序列
 * @in_order 中序遍历序列
 * @len 序列长度
 */
void construct(const T pre_order[], const T in_order[], const unsigned int len) {
    if (len < 1)
        return;

    this->root = construct_core(pre_order, pre_order + len - 1, 
            in_order, in_order + len - 1);
}

/*
 * 通过前序遍列和中序遍历的结果构建二叉树
 * 前序遍历序列[pre_order_start, pre_order_end],中序遍历序列[in_order_start, in_order_end]
 * @pre_order_start 前序遍历的开始位置
 * @pre_order_end 前序遍历的结束位置
 * @in_order_start 中序遍历的开始位置
 * @in_order_end 中序遍历的结束位置
 */
tree_node<T> *construct_core(const T *pre_order_start, const T *pre_order_end,
                             const T *in_order_start, const T *in_order_end) {
    // 遍历指针
    const T *cursor;
    // 前序遍历序列中左子树的结束位置
    const T *lchild_end;
    // 左子树的长度
    unsigned int lchild_len;
    // 根节点
    tree_node<T> *root = nullptr;;

    // 前序遍历的首元素即为树根
    root = new tree_node<T>(pre_order_start[0]);

    // 如果前序遍历的头和尾相等,说明只有一个元素了,返回当前元素
    if (pre_order_start == pre_order_end) {
        // 下面是简单的判断输入参数是否合法
        // 如果只有一个元素了,前序和中序的遍历结果是相同的
        if (in_order_start == in_order_end && *in_order_start == *pre_order_start)
            return root;
        else // 输入有问题
            throw "invalid input";
    }

    // 在中序遍历中找到根节点
    cursor = in_order_start;
    while (cursor < in_order_end && *cursor != root->data)
        cursor++;

    // 匹配到中序遍历的最后一个元素了还没有找到,说明输入不合法
    if (cursor == in_order_end && *cursor != root->data)
        throw "invalid input";

    lchild_len = cursor - in_order_start;
    lchild_end = pre_order_start + lchild_len;

    // 递归获取左子树
    if (lchild_len > 0) {
        root->lchild = construct_core(pre_order_start + 1, lchild_end,
                                      in_order_start, cursor - 1);
    }

    // 递归获取右子树
    if (lchild_len < pre_order_end - pre_order_start) {
        root->rchild = construct_core(lchild_end + 1, pre_order_end,
                                      cursor + 1, in_order_end);
    }

    return root;
}

四、单元测试

单元测试覆盖以下几点:

  • 树为空
  • 树只有一个节点
  • 完全二叉树
  • 满二叉树
  • 不完全二叉树

4.1 树为空

/*
 * 测试:通过前序和中序遍历序列构造二叉树
 * 案例:二叉树为空
 */
TEST(construct_by_pre_order_and_in_order, no_node) {
    bin_tree<int> tree;
    tree.construct(nullptr, nullptr, 0);
    EXPECT_FALSE(tree.get_root());
}

4.2 树只有一个节点

/*
 * 测试:通过前序和中序便利序列构造二叉树
 * 案例:二叉树包含一个节点
 */
TEST(construct_by_pre_order_and_in_order, one_node) {
    bin_tree<int> tree;
    int data[1] = {1};
    tree.construct(data, data, 1);
    EXPECT_TRUE(tree.get_root());
    EXPECT_FALSE(tree.get_root()->get_lchild());
    EXPECT_FALSE(tree.get_root()->get_rchild());
}

4.3 完全二叉树

/*
 * 测试:通过前序和中序便利序列构造二叉树
 * 案例:完全二叉树测试
 *          1
 *        2   3
 *       4 5
 */
TEST(construct_by_pre_order_and_in_order, complete_bin_tree) {
    int pre_order[7] = {1, 2, 4, 5, 3};
    int in_order[7] = {4, 2, 5, 1, 3};
    bin_tree<int> tree;
    const tree_node<int> *root, *node;
    tree.construct(pre_order, in_order, 5);
    root = tree.get_root();
    EXPECT_TRUE(root);

    EXPECT_EQ(root->get_data(), 1);
    EXPECT_TRUE(root->get_lchild());
    EXPECT_TRUE(root->get_rchild());
    EXPECT_EQ(root->get_lchild()->get_data(), 2);
    EXPECT_EQ(root->get_rchild()->get_data(), 3);

    node = root->get_lchild(); // -> 节点2
    // 检查子节点
    EXPECT_TRUE(node->get_lchild());
    EXPECT_TRUE(node->get_rchild());
    EXPECT_EQ(node->get_lchild()->get_data(), 4);
    EXPECT_EQ(node->get_rchild()->get_data(), 5);
    // 检查子节点的子节点
    EXPECT_FALSE(node->get_lchild()->get_lchild());
    EXPECT_FALSE(node->get_lchild()->get_rchild());
    EXPECT_FALSE(node->get_rchild()->get_lchild());
    EXPECT_FALSE(node->get_rchild()->get_rchild());

    node = root->get_rchild(); // -> 节点3
    // 检查子节点
    EXPECT_FALSE(node->get_lchild());
    EXPECT_FALSE(node->get_rchild());
}

4.4 满二叉树

/*
 * 测试:通过前序和中序便利序列构造二叉树
 * 案例:满二叉树测试
 *          1
 *        2   3
 *       4 5 6 7
 */
TEST(construct_by_pre_order_and_in_order, full_bin_tree) {
    int pre_order[7] = {1, 2, 4, 5, 3, 6, 7};
    int in_order[7] = {4, 2, 5, 1, 6, 3, 7};
    bin_tree<int> tree;
    const tree_node<int> *root, *node;
    tree.construct(pre_order, in_order, 7);
    root = tree.get_root();
    EXPECT_TRUE(root);

    EXPECT_EQ(root->get_data(), 1);
    EXPECT_TRUE(root->get_lchild());
    EXPECT_TRUE(root->get_rchild());
    EXPECT_EQ(root->get_lchild()->get_data(), 2);
    EXPECT_EQ(root->get_rchild()->get_data(), 3);

    node = root->get_lchild(); // -> 节点2
    // 检查子节点
    EXPECT_TRUE(node->get_lchild());
    EXPECT_TRUE(node->get_rchild());
    EXPECT_EQ(node->get_lchild()->get_data(), 4);
    EXPECT_EQ(node->get_rchild()->get_data(), 5);
    // 检查子节点的子节点
    EXPECT_FALSE(node->get_lchild()->get_lchild());
    EXPECT_FALSE(node->get_lchild()->get_rchild());
    EXPECT_FALSE(node->get_rchild()->get_lchild());
    EXPECT_FALSE(node->get_rchild()->get_rchild());

    node = root->get_rchild(); // -> 节点3
    // 检查子节点
    EXPECT_TRUE(node->get_lchild());
    EXPECT_TRUE(node->get_rchild());
    EXPECT_EQ(node->get_lchild()->get_data(), 6);
    EXPECT_EQ(node->get_rchild()->get_data(), 7);
    // 检查子节点的子节点
    EXPECT_FALSE(node->get_lchild()->get_lchild());
    EXPECT_FALSE(node->get_lchild()->get_rchild());
    EXPECT_FALSE(node->get_rchild()->get_lchild());
    EXPECT_FALSE(node->get_rchild()->get_rchild());
}

4.5 非完全二叉树

/*
 * 测试:通过前序和中序便利序列构造二叉树
 * 案例:普通二叉树测试
 *          1
 *        2   3
 *         5 6
 */
TEST(construct_by_pre_order_and_in_order, normal_bin_tree) {
    int pre_order[7] = {1, 2, 5, 3, 6};
    int in_order[7] = {2, 5, 1, 6, 3};
    bin_tree<int> tree;
    const tree_node<int> *root, *node;
    tree.construct(pre_order, in_order, 5);
    root = tree.get_root();
    EXPECT_TRUE(root);

    EXPECT_EQ(root->get_data(), 1);
    EXPECT_TRUE(root->get_lchild());
    EXPECT_TRUE(root->get_rchild());
    EXPECT_EQ(root->get_lchild()->get_data(), 2);
    EXPECT_EQ(root->get_rchild()->get_data(), 3);

    node = root->get_lchild(); // -> 节点2
    // 检查子节点
    EXPECT_FALSE(node->get_lchild());
    EXPECT_TRUE(node->get_rchild());
    EXPECT_EQ(node->get_rchild()->get_data(), 5);
    // 检查子节点的子节点
    EXPECT_FALSE(node->get_rchild()->get_lchild());
    EXPECT_FALSE(node->get_rchild()->get_rchild());

    node = root->get_rchild(); // -> 节点3
    // 检查子节点
    EXPECT_TRUE(node->get_lchild());
    EXPECT_FALSE(node->get_rchild());
    // 左子节点为6,右子节点为空
    EXPECT_EQ(node->get_lchild()->get_data(), 6);
    // 检查子节点的子节点
    EXPECT_FALSE(node->get_lchild()->get_lchild());
    EXPECT_FALSE(node->get_lchild()->get_rchild());
}

4.6 测试结果

关于斐波那契数列的描述:
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

一、面试题

求斐波那契数列的第n项:写一个函数,输入n,输出斐波那契数列的第n项。

二、解法一

求斐波那契数列最简单的解法就是用递归,大部分书籍上介绍递归的时候都会用斐波纳契数列做例子,递归很容易写:

long long Fibonacci_Solution1(unsigned int n)
{
    if(n <= 0)
        return 0;

    if(n == 1)
        return 1;

    return Fibonacci_Solution1(n - 1) + Fibonacci_Solution1(n - 2);
}

这个解法并不是最优的,因为中间充斥了很多重复的运算,应该还不足以应付面试。例如计算f(10)的过程中:

f(6)/f(7)/f(8)都被计算了多次,当数据越大时,重复计算的次数也越来越多,因此并不是最优的解法。

三、解法二

上面的递归之所以慢,是因为重复的计算太多,我们可以考虑从如何减少重复计算下手。例如可以选择从2开始,保存前两位的值,依次向后计算:

long long Fibonacci_Solution2(unsigned n)
{
    int result[2] = {0, 1};
    if(n < 2)
        return result[n];

    long long  fibNMinusOne = 1;
    long long  fibNMinusTwo = 0;
    long long  fibN = 0;
    for(unsigned int i = 2; i <= n; ++ i)
    {
        fibN = fibNMinusOne + fibNMinusTwo;

        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }

     return fibN;
}

注意事项:传参的n的类型为unsigned int,保存值以及返回值的数据类型是long long,避免int溢出。

ps:虽然long long也会溢出,至少比int大一些。