C++编写算法(六)——查找问题,二叉查找树

一、二叉查找树

前面一章分析了二分查找这种算法。

要支持高效的插入操作,我们需要链式的数据结构,但是链表不能二分查找,因为中间值需要从链表头部开始寻找。为了将二分查找的效率和链表的灵活性结合起来,需要一种新的数据结构。
这种数据结构就是二叉树查找,它能够将链表插入的灵活性和有序数组查找的高效性结合起来的符号表实现。

要实现二叉树,首先树的结构需要了解清楚,一般一个树可以分为两个部分:

树根 ---> 树节点

树节点中又存储着几个部分:

存储的Key值 -- 存储的value值 -- 节点左孩子 -- 节点右孩子

// header.h
#ifndef JUNZALGHEADER_H_
#define JUNZALGHEADER_H_
#include <iostream>
#include <queue>
template<class T>
struct TreeNode
{
    T key;
    int data;
    TreeNode* leftChild = nullptr;
    TreeNode* rightChild = nullptr;
};

template<class T>
class BinaryTree
{
protected:
    TreeNode<T> * root;
public:
    BinaryTree();
//  BinaryTree(T k, int d, int n);
    void SetTreeRoot(const TreeNode<T> * r);
    void SetTreeNnum(int n);
    TreeNode<T>* getRoot() { return root; }

    // Ergodic
    void midOrder();
    void midOrder_ac(TreeNode<T> * currentNode);

    void preOrder();
    void preOrder_ac(TreeNode<T> * currentNode);

    void postOrder();
    void postOrder_ac(TreeNode<T> * currentNode);

    void layerOrder();

    void Show(TreeNode<T> * currentNode) const;
    // insert
    bool treeInsert(TreeNode<T> * currentNode);

    // find
    int treeFind(TreeNode<T> * currentNode);
};
#endif // !JUNZALGHEADER_H_

template<class T>
BinaryTree<T>::BinaryTree()
{
    root = NULL;
}

template<class T>
void BinaryTree<T>::SetTreeRoot(const TreeNode<T> * r)
{
    root = r;
    if (num == 0)
        num += 1;
    else
        num = 1;
}

template<class T>
void BinaryTree<T>::SetTreeNnum(int n)
{
    num = n;
}

template<class T>
void BinaryTree<T>::Show(TreeNode<T> * currentNode) const
{
    std::cout << currentNode->key;
}

template<class T>
void BinaryTree<T>::midOrder()
{
    midOrder_ac(root);
}

template<class T>
void BinaryTree<T>::midOrder_ac(TreeNode<T> * currentNode)
{
    if (currentNode)
    {
        midOrder_ac(currentNode->leftChild);
        Show(currentNode);
        midOrder_ac(currentNode->rightChild);
    }
    else
    {
        return;
    }
}

template<class T>
void BinaryTree<T>::preOrder()
{
    preOrder_ac(root);
}

template<class T>
void BinaryTree<T>::preOrder_ac(TreeNode<T> * currentNode)
{
    if (currentNode)
    {
        Show(currentNode);
        preOrder_ac(currentNode->leftChild);
        preOrder_ac(currentNode->rightChild);
    }
    else
        return;
}

template<class T>
void BinaryTree<T>::postOrder()
{
    postOrder_ac(root);
}

template<class T>
void BinaryTree<T>::postOrder_ac(TreeNode<T>* currentNode)
{
    if (currentNode)
    {
        postOrder_ac(currentNode->leftChild);
        postOrder_ac(currentNode->rightChild);
        Show(currentNode);
    }
    else
        return;
}

template<class T>
void BinaryTree<T>::layerOrder()
{
    std::queue<TreeNode<T>*> q;
    TreeNode<T> *currentNode = root;
    while (currentNode)
    {
        Show(currentNode);
        if(currentNode->leftChild)
            q.push(currentNode->leftChild);
        if(currentNode->rightChild)
            q.push(currentNode->rightChild);
        if (q.empty()) break;
        currentNode = q.front();
        q.pop();
    }
}

template<class T>
bool BinaryTree<T>::treeInsert(TreeNode<T> *currentNode)
{
    if (root == NULL)
    {
        root = currentNode;
        return true;
    }
    else
    {
        TreeNode<T> * NowNode = root;
        while (NowNode)
        {
            if (currentNode->key < NowNode->key)
            {
                if (NowNode->leftChild)
                {
                    NowNode = NowNode->leftChild;
                }
                    
                else
                {
                    NowNode->leftChild = currentNode;
                    break;
                }
            }
            else if (currentNode->key > NowNode->key)
            {
                if (NowNode->rightChild)
                    NowNode = NowNode->rightChild;
                else
                {
                    NowNode->rightChild = currentNode;
                    break;
                }
            }
            else
            {
                NowNode->data = currentNode->data;
                break;
            }
        }
        return true;
    }
}

template<class T>
int BinaryTree<T>::treeFind(TreeNode<T> *currentNode)
{

    if (root == NULL)
    {
        return -1;
    }
    else
    {
        TreeNode<T> *NowNode = root;
        while (NowNode)
        {
            if (currentNode->key == NowNode->key)
            {
                NowNode->data = currentNode->data;
                return NowNode->data;
            }
            else if (currentNode->key > NowNode->key)
                NowNode = NowNode->rightChild;
            else if (currentNode->key < NowNode->key)
                NowNode = NowNode->leftChild;
        }
        return NowNode->data;
    }
}
// main.cpp
#include<iostream>
#include "JunzAlgHeader.h"
using namespace std;
const int NUM = 7;
int main()
{
    BinaryTree<char> tree;
    TreeNode<char> temp[NUM];
    for (int i = 0; i < NUM; i++)
    {
        cout << "Add the treenode: \n";
        cout << "Key: (char)";
        cin >> (temp[i].key);
        cout << "Value:(int)";
        cin >> (temp[i].data);
        // tree insert
        tree.treeInsert(&temp[i]);
    }

    cout << "Middle Order Ergodic: \n";
    tree.midOrder();
    cout << endl;
    cout << "Layer Order Ergodic: \n";
    tree.layerOrder();
    cout << endl;

    cout << "Tree find:\n";
    TreeNode<char> temp1;
    temp1.data = 4;
    temp1.key = 'A';
    cout << tree.treeFind(&temp1) << endl;
    TreeNode<char> temp2;
    temp2.data = 2;
    temp2.key = 'A';
    cout << tree.treeFind(&temp2) <<endl;
    system("pause");
    return 0;
}

编写这个程序是遇到一个问题:就是使用常规的头文件声明类及方法、.cpp文件定义类方法、main函数执行功能这个套路是行不通的,程序会报Linker Tools Error LNK2019的错误。
原因:采用的是特殊的模板类进行代码的编写。

编译器处理模板类时,如有一些模板类以及函数,则会进行如下操作。
1、模板函数和类在编译时期间并未生成具体的二进制代码, 在main函数中也没有嵌入这个类或函数的代码,只是包含了一句 call +类名/函数名
2、编译阶段,在main函数中发现了模板类和的引用,但是main.obj


有序性相关方法和删除操作

1、首先,我们可以获取树的最大键和最小键。

其思路非常简单,最小键即处于树最左边的键,最大键即处于树最右边的键。获取最小键与最大键的方法我们可以采用递归来实现。

 // ac
    TreeNode<T> * Min_ac(TreeNode<T> *currentNode);
    TreeNode<T> * Max_ac(TreeNode<T> *currentNode);
// min & max find
    T & Min();
    T & Max();

// definition
//private Min methods
template<class T>
TreeNode<T>* BinaryTree<T>::Min_ac(TreeNode<T> * currentNode)
{
    if (currentNode->leftChild == NULL)
        return currentNode;
    return Min_ac(currentNode->leftChild);
}
//private Max methods
template<class T>
TreeNode<T>* BinaryTree<T>::Max_ac(TreeNode<T> * currentNode)
{
    if (currentNode->rightChild == NULL)
        return currentNode;
    return Max_ac(currentNode->rightChild);
}

// public Min
template<class T>
T & BinaryTree<T>::Min()
{
    return (Min_ac(root)->key);
}

//public Max
template<class T>
T & BinaryTree<T>::Max()
{
    return (Max_ac(root)->key);
}
2、向上取整和向下取整

在向下取整中,若需要判断的键Key到达某个节点时,它需要寻找该节点的左子树,因为小于等于key的键值就在该节点的左子树中。只有当右子树存在比Key小的键值时,最大的小于等于键值的键才会出现在右子树中。因此,利用这个思路,可以写出floor向下取整函数。当然,向上取整就是把上述思路的左右对调一下。

// ac
    TreeNode<T> * floor_ac(TreeNode<T> *currentNode, T k);
    TreeNode<T> * ceiling_ac(TreeNode<T> *currentNode, T k);
    T floor(T k);
    T ceiling(T k);
//
// private floor method
template<class T>
TreeNode<T> * BinaryTree<T>::floor_ac(TreeNode<T> * currentNode, T k)
{
    if (currentNode == NULL)
        return NULL;
    if (k < currentNode->key)
        return floor_ac(currentNode->leftChild, k);
    if (k == currentNode->key)
        return currentNode;
    TreeNode<T> * temp;
    temp = floor_ac(currentNode->rightChild, k);
    if (temp != NULL)
        return temp;
    else
        return currentNode;
}

// private ceiling method
template<class T>
TreeNode<T> * BinaryTree<T>::ceiling_ac(TreeNode<T> *currentNode, T k)
{
    if (currentNode == NULL)
        return NULL;
    if (k > currentNode->key)
        return ceiling_ac(currentNode->rightChild, k);
    if (k == currentNode->key)
        return currentNode;
    TreeNode<T> *temp;
    temp = ceiling_ac(currentNode->leftChild, k);
    if (temp != NULL)
        return temp;
    else
        return currentNode;
}

// public floor methods
template<class T>
T BinaryTree<T>::floor(T k)
{
    TreeNode<T> * Node = floor_ac(root, k);
    if (Node == NULL)
        return NULL;
    return Node->key;
}

//public ceiling methods
template<class T>
T BinaryTree<T>::ceiling(T k)
{
    TreeNode<T>* Node = ceiling_ac(root, k);
    if (Node == NULL)
        return NULL;
    return Node->key;
}

3、选择操作

选择操作是将排名为r的节点的key值输出。具体思路是:
①从根节点开始,计算其左子树的结点数。
②如果左子树的结点数小于r,则继续往左子树寻找排名为r的结点。
③如果左子树的结点数大于r,则向其右子树中寻找排名为r-左子树结点数-1的结点
④如果左子树的结点数等于r,则返回其左子树的根结点。
⑤递归1~4过程,直至找到相应结点。

// private select method
template<class T>
TreeNode<T> * BinaryTree<T>::select_ac(TreeNode<T> *currentNode, int k)
{
    if (currentNode == NULL)
        return NULL;
    int size_leftChild = size(currentNode->leftChild);
    if (size_leftChild > k)
        return select_ac(currentNode->leftChild, k);
    else if (size_leftChild < k)
        return select_ac(currentNode->rightChild, k - size_leftChild - 1);
    else
        return currentNode;
}
//public select method
template<class T>
T BinaryTree<T>::select(int k)
{
    return select_ac(root, k)->key;
}

4、排名操作

排名操作其实是选择操作的逆过程。即它是通过查找结点中的key值,计算该结点有r个比它的key值小的结点,从而得出排名r。 具体思路是:
①对比需要查找的key值与当前根结点的key值。
②如果查找key值小于根结点key值,则继续在根结点的左子树寻找。
③如果查找的key值大于根结点的key值,则将排名更新至1+当前根节点左子树的结点数,再在右子树寻找。
④如果查找的key值等于根结点的key值,则排名即为其左子树的结点数。
⑤递归1~4过程,直至找到相应结点,返回排名。

// private rank method
template<class T>
int BinaryTree<T>::rank_ac(TreeNode<T> *currentNode, T ke)
{
    int size_temp = size(currentNode->leftChild);
    if (ke == currentNode->key)
        return size_temp;
    else if (ke < currentNode->key)
        return rank_ac(currentNode->leftChild, ke);
    else
        return 1 + size_temp + rank_ac(currentNode->rightChild, ke);

}
template<class T>
int BinaryTree<T>::rank(T ke)
{
    return rank_ac(root, ke);
}

5、删除最大键和删除最小键

删除最小(大)键首先需要找到最小(大)键。所用的思路与上述的Min()和Max()函数类似。
寻找到最小(大)键后,需要对其进行删处,并将它的右子树连接到最小(大)键的父结点中。

// private delete min method
template<class T>
TreeNode<T> * BinaryTree<T>::deleteMin_ac(TreeNode<T> * currentNode)
{
    if (currentNode->leftChild == NULL)
        return currentNode->rightChild;
    currentNode->leftChild = deleteMin_ac(currentNode->leftChild);
    return currentNode;
}
// public delete min
template<class T>
bool BinaryTree<T>::deleteMin()
{
    if (root)
    {
        root = deleteMin_ac(root);
        return true;
    }
    else
    {
        return false;
    }
}
// private delete max method
template<class T>
TreeNode<T> * BinaryTree<T>::deleteMax_ac(TreeNode<T> * currentNode)
{
    if (currentNode->rightChild == NULL)
        return currentNode->rightChild;
    currentNode->rightChild = deleteMax_ac(currentNode->rightChild);
    return currentNode;
}
// public delete max
template<class T>
bool BinaryTree<T>::deleteMax()
{
    if (root)
    {
        root = deleteMax_ac(root);
        return true;
    }
    else
        return false;
}

小记:寻找与删除最小(大)结点的思路是比较容易的,唯一的难题在于如何将最小(大)结点的右子树连接到最小(大)结点的父结点上,保持树的有序性。

currentNode->leftChild = deleteMin_ac(currentNode->leftChild);

这个句式巧妙地解决了这个难题,(以删除最小叶结点为例)通过每一层的递归,将递归的结果付给当前遍历结点的左指针。在未找到最小值前,deleteMin_ac()函数当前层会返回该叶结点的原左子结点,保护了未被操作的子结点。当寻找到最小值时,deleteMin_ac()函数返回最小叶结点的右子结点,返回上层之后自然能够将最小叶节点的父结点与最小叶结点的右子结点相连。此时,最小叶结点输入失连状态,被系统自动回收。
(删除最大叶结点的原理也如此)


6、删除树中的某个结点

删除树中的某个结点是树中最难的操作,根据理解,删除树中结点需要分为4个步骤:
①寻找需要删除的节点。
②找到需要删除的节点的位置后,查找其右子树的最小叶节点t。
③t的右指针指向需要删除的节点的右子树根节点,t的左指针指向需要删除的节点的左指针。
④将需要删除的节点的父结点左/右指针指向t。

// private delete node assisted method
template<class T>
TreeNode<T> * BinaryTree<T>::delete_as(TreeNode<T> * currentNode, T ke)
{
    if (currentNode == NULL)
        return NULL;
    if (ke < currentNode->key)
        currentNode->leftChild = delete_as(currentNode->leftChild, ke);
    else if (ke > currentNode->key)
        currentNode->rightChild = delete_as(currentNode->rightChild, ke);
    else
    {
        if (currentNode->leftChild == NULL)
            return currentNode->rightChild;
        if (currentNode->rightChild == NULL)
            return currentNode->leftChild;
        TreeNode<T> *t = currentNode;
        currentNode = Min_ac(t->rightChild);
        currentNode->rightChild = deleteMin_ac(t->rightChild);
        currentNode->leftChild = t->leftChild;
    }
    return currentNode;
}
// public deleteNode
template<class T>
bool BinaryTree<T>::deleteNode(T ke)
{
    if (root)
    {
        delete_as(root, ke);
        return true;
    }

    else
        return false;
    
}

小记:编写删除方法的思路十分清晰。但是有一点困扰了我很久,思路③中左指针和右指针的分配是有顺序的。从程序上看,如果

// code1
currentNode->rightChild = deleteMin_ac(t->rightChild);
currentNode->leftChild = t->leftChild;

调换顺序为

// code2
currentNode->leftChild = t->leftChild;
currentNode->rightChild = deleteMin_ac(t->rightChild);

那么,在树图。。。中,code2会丢失结点A,导致遍历程序无休止的运行。其原因是本身结点H的左孩结点为NULL,在上述程序中,为了删除结点E,需要把结点A的父结点修改为结点H。code2执行第一行时是没有问题的,但执行第二行时,因为H的左子结点已经赋为结点A,则deleteMin_ac函数旨在寻找最小结点,因此需要寻找E的右子树的最左的子结点。在原来的树中,E的右子树的最小结点为H,而执行了code2第一行的操作后,E的右子树的最小结点变为A,因此删除A后,将C接到H的左指针处。H的右指针接到结点R,造成整个程序的混乱。因此,将code2代码改为code1,才能实现正确删除结点的功能。


7、范围查找

范围查找是输入一个区间,如[F,T]。遍历整个树,找到落入该范围的节点。因此,范围查找与层序遍历有一些类似,相类似的地方有两点:①两者同样需要遍历整个树,②遍历整个树都需要引入队列的思想。范围查找的具体步骤为:
(1)遍历整个树
(2)寻找队列中落入区间的叶结点
(3)将这些落入区间的叶结点以队列(类似与层序)的形式输出

// public search method
template <class T>
void BinaryTree<T>::keys_ac(std::queue<TreeNode<T>*> &q, T lo, T hi)
{
    if (root == NULL)
        return;
    std::queue<TreeNode<T>*> temp_saver;
    TreeNode<T>* ptNode = root;
    while (ptNode)
    {
        if (ptNode->key >= lo && ptNode->key <= hi)
            q.push(ptNode);
        if (ptNode->leftChild)
            temp_saver.push(ptNode->leftChild);
        if (ptNode->rightChild)
            temp_saver.push(ptNode->rightChild);
        if (temp_saver.empty()) break;
        ptNode = temp_saver.front();
        temp_saver.pop();
    }
}

小记:范围查找需要采用两个队列进行实现,其中一个队列用于遍历整个树(temp_saver),另一个队列用于存储落入区间的叶结点(q)。采用函数引用队列q (queue & q),便于修改队列q的内容。在函数调用之后输出队列q即能够找到落入区间的叶结点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容