数据结构——树

一、树

树是n(n≥0)个结点的有限集合。n=0时称为空树。在任意一棵非空树中:
(1)有且仅有一个特定的称为根(root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又有一棵树,并且称为根的子树。
结点拥有的子树数称为结点的度(degree)。度为0的结点称为叶结点(leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值树中结点的最大层次称为树的深度(depth)或高度
如果将树中结点的各子树看成从左至右是有序的,不能互换的,则称该树为有序树,否则称为无序树。
森林(forest)是m(m≥0)棵互不相交的树的集合

树的存储结构的常用表示法是孩子表示法,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,即用多重链表来表示树
树的常见操作有
(1)初始化,建立一个空的树;
(2)销毁树;
(3)按照给定内容创建一棵树;
(4)将树中数据清空;
(5)判断树是否为空;
(6)获得树的深度;
(7)获得树的根结点;
(8)获得树中给定结点处的数据值;
(9)将树中给定结点处赋值为给定值;
(10)获得树的指定结点的父结点;
(11)获得树的指定结点的左孩子结点;
(12)获得树的指定结点的右孩子结点;
(13)在树中指定结点处插入给定值作为新的结点
(14)删除树中的指定结点
(15)查找在树的结点中是否有结点的数据值与给定值相等
(16)树的所有结点的顺序遍历

二、二叉树

普通树的操作是很复杂的,且没有良好的性质,一般不常用。常用的是树的一个特例,二叉树。
二叉树(binary tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的,分别称为根结点的左子树和右子树的二叉树组成。
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树
二叉树具有以下性质:
(1)在二叉树的第i层上至多有2^(i-1)个结点(i≥1)。
(2)深度为k的二叉树至多有2^k-1个结点(k≥1)。
(3)对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
(4)具有n个结点的完全二叉树的深度为floor(log2N)+1。
(5)如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到最后一层,每层从左到右),对任一结点i(1≤i≤n)有:
①如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点floor(i/2);
②如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i;
③如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
二叉树一般用二叉链表表示,二叉链表的结点结构为:

template<typename T>
class Node
{
public:
    T data;
    Node *lchild, *rchild;
    Node(T _data)
    {
        data = _data;
        lchild = nullptr;
        rchild = nullptr;
    }
    ~Node()
    {
        if (lchild != nullptr)
        {
            delete lchild;
        }
        if (rchild != nullptr)
        {
            delete rchild;
        }
    }
};
template<typename T>
class BinaryTree
{
private:
    Node *root;
public:
    BinaryTree()
    {
        root = nullptr;
    }
    ~BinaryTree()
    {
        delete root;
    }
    void build_demo()
    {
        root = new Node(1);
        root->lchild = new Node(2);
        root->rchild = new Node(3);
        root->lchild->lchild = new Node(4);
        root->lchild->rchild = new Node(5);
        root->rchild->rchild = new Node(6);
    }
};

二叉树的常见创建方式有:

// 根据A(,C)形式的字符串构建二叉树
template<typename T>
Node<T> *str2Tree(const string &str)
{
    int len = str.length();
    if (len == 0)
    {
        return nullptr;
    }
    Node<T> *p = new Node<T>(str[0]);
    Node<T> *finalPtr = p;
    stack<Node<T>*> s;
    int state = 0;
    for (int i = 1; i < len; ++i)
    {
        switch (str[i])
        {
        case '(':
            s.push(p);
            state = 1;
            break;
        case ')':
            s.pop();
            break;
        case ',':
            state = 2;
            break;
        default:
            p = new Node<T>(str[i]);
            switch (state)
            {
            case 1:
                s.top()->lchild = p;
                break;
            case 2:
                s.top()->rchild = p;
                break;
            default:
                break;
            }
            break;
        }
    }
    return finalPtr;
}
// 根据二叉树输出A(,C)形式的字符串
template<typename T>
void tree2Str(Node<T> *root)
{
    cout << root->data;
    if (root->lchild != nullptr)
    {
        cout << "(";
        tree2Str(root->lchild);
        if (root->rchild == nullptr)
        {
            cout << ")";
        }
    }
    if (root->rchild != nullptr)
    {
        if (root->lchild == nullptr)
        {
            cout << "(";
        }
        cout << ",";
        tree2Str(root->rchild);
        cout << ")";
    }
}
// 根据层序遍历的字符串构建完全二叉树
// 层序是一个连续字符串,每个字符代表一个结点值
template<typename T>
Node<T> *levelStr2Tree(const string &str, int start, int len)
{
    if (start >= len)
    {
        return nullptr;
    }
    Node<T> *p = new Node<T>(str[start]);
    p->lchild = levelStr2Tree(str, 2 * start + 1, len);
    p->rchild = levelStr2Tree(str, 2 * start + 2, len);
    return p;
}

二叉树的遍历(traverse)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点都被访问一次且仅被访问一次。
二叉树的遍历常见有4种形式:
(1)前序遍历:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
(2)中序遍历:若二叉树为空,则空操作返回,否则先中序遍历左子树,然后访问根结点,最后中序遍历右子树。
(3)后序遍历:若二叉树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点。
(4)层序遍历:若二叉树为空,则空操作返回,否则从树的第一层开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。

template<typename T>
void preOrder(Node<T> *root)
{
    if (!root)
    {
        return;
    }
    cout << root->data << " ";
    preOrder(root->lchild);
    preOrder(root->rchild);
}

// 中序遍历
template<typename T>
void inOrder(Node<T> *root)
{
    if (!root)
    {
        return;
    }
    inOrder(root->lchild);
    cout << root->data << " ";
    inOrder(root->rchild);
}

// 后序遍历
template<typename T>
void postOrder(Node<T> *root)
{
    if (!root)
    {
        return;
    }
    postOrder(root->lchild);
    postOrder(root->rchild);
    cout << root->data << " ";
}

// 层序遍历
template<typename T>
void levelOrder(Node<T> *root)
{
    cout << root->data;
    queue<Node<T>*> q;
    if (root->lchild != nullptr)
    {
        q.push(root->lchild);
    }
    if (root->rchild != nullptr)
    {
        q.push(root->rchild);
    }
    Node<T> *temp;
    while (!q.empty())
    {
        temp = q.front();
        cout << " " << temp->data;
        q.pop();
        if (temp->lchild != nullptr)
        {
            q.push(temp->lchild);
        }
        if (temp->rchild != nullptr)
        {
            q.push(temp->rchild);
        }
    }
}

已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。但已知前序和后序遍历序列,不能确定一棵二叉树

// 已知先序和中序求二叉树
// 先序和中序都是一个连续字符串,每个字符代表一个结点值
// 参数 len为字符串长度 即结点个数
template<typename T>
Node<T> *preIn2Tree(const string &preStr, const string &inStr, int len)
{
    Node<T> *p = new Node<T>(preStr[0]);
    int pos = inStr.find(preStr[0]);
    if (pos > 0)
    {
        p->lchild = preIn2Tree(preStr.substr(1, pos), inStr.substr(0, pos), pos);
    }
    if (len - pos - 1 > 0)
    {
        p->rchild = preIn2Tree(preStr.substr(pos + 1), inStr.substr(pos + 1), len - pos - 1);
    }
    return p;
}
// 已知中序和后序求二叉树
// 中序和后序都是一个连续字符串,每个字符代表一个结点值
// 参数 len为字符串长度 即结点个数
template<typename T>
Node<T> *inPost2Tree(const string &inStr, const string &postStr, int len)
{
    Node<T> *p = new Node<T>(postStr[len-1]);
    int pos = inStr.find(postStr[len-1]);
    if (pos > 0)
    {
        p->lchild = inPost2Tree(inStr.substr(0, pos),    postStr.substr(0, pos), pos);
    }
    if (len - pos - 1 > 0)
    {
        p->rchild = inPost2Tree(inStr.substr(pos + 1), postStr.substr(pos), len - pos - 1);
    }
    return p;
}

指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(threaded binary tree)。线索二叉树等于是把一棵二叉树转变成一个双向链表。
如果所用二叉树需要经常遍历或查找结点时,需要某种遍历序列中的前驱和后继,则可采用线索二叉链表。

借助二叉链表,树和二叉树可以相互进行转换,甚至表示森林都是可以的,森林与二叉树也可以互相进行转换

树的遍历分为两种方式:
(1)一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。
(2)另一种是后根遍历树,即先依次后根遍历每棵子树,然后再访问根的结点。
森林的遍历也分为两种方式
(1)前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。
(2)后序遍历:先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。

从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称为路径长度。树的路径长度就是从树根到每一结点的路径长度之和。如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。则其中带权路径长度最小的二叉树称为赫夫曼树
赫夫曼树主要用于数据压缩。先统计出每种字母在字符串里出现的频率,根据频率建立一棵路径带权的二叉树,即为赫夫曼树。树上每个结点存储字母出现的频率,根结点到结点的路径即是字母的编码,频率高的字母使用较短的编码,频率低的字母使用较长的编码,这样使得编码后的字符串占用空间最小。

三、二叉查找树

二叉树的一个重要应用是在一系列数据中查找数据
二叉查找树(binary search tree)或者是一棵空树,或者是具有以下性质的二叉树:
(1)若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)它的左、右子树也分别为二叉查找树。
构造二叉查找树的目标并不是为了排序,而是为了提高查找和插入、删除关键字结点的速度。二叉查找树的插入和查找效率相对较高,最坏情况下时间复杂度为O(n),期望的时间复杂度为O(logn)。
在二叉查找树中,如果删除的次数不多,通常使用的策略是懒惰删除(lazy deletion):当一个元素要被删除时,它仍留在树中,只是做个被删除的记号。这种做法在有重复关键字的时候非常流行。

平衡二叉树(self-balancing binary search tree)是一种二叉查找树,其中每一个结点的左子树和右子树的高度差至多等于1。将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子(balance factor),则平衡二叉树每个结点的平衡因子只可能是-1,0,1。
平衡二叉树构建的基本思想就是在构建二叉查找树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是则找出最小不平衡子树。在保持二叉查找树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
平衡二叉树结构的插入和查找效率均为O(logn)
所有平衡二叉树由3个特征组成:
(1)自平衡条件;
(2)旋转操作;
(3)旋转的触发。
AVL(Adelson-Velskii-Landis)树是带有平衡条件的二叉查找树。一棵AVL树是其每个结点的左子树和右子树的高度最多差1的二叉查找树。
在插入一个元素后不断回溯的过程中,如果因此导致节点不平衡,则根据不平衡情况进行对应的旋转:
(1)左子树比右子树的高度大2:
①如果新增元素插入到左儿子的左子树,则进行右旋操作。(LL型调整)
②如果新增元素插入到左儿子的右子树,则进行左旋加右旋操作。(LR型调整)
(2)右子树比左子树的高度大2:
①如果新增元素插入到右儿子的右子树中,则进行左旋操作。(RR型调整)
②如果新增元素插入到右儿子的左子树中,则进行右旋加左旋操作。(RL型调整)
类型的,在删除一个元素后不断回溯的过程中,如果因此导致节点不平衡,则和插入操作采用相同的调整操作,确保在删除以后整棵树依然是平衡的。

AVL树的一个流行的变种是红黑树(red-black tree)。其是具有以下性质的二叉查找树:
(1)每一个结点或者是红色,或者是黑色;
(2)树的根结点是黑色的;
(3)树的叶结点是黑色的;
(4)如果一个结点是红色的,则它的子结点必须是黑色的;
(5)从一个结点到一个NULL指针的每一条路径必须包含相同数目的黑色结点。
红黑树相比于AVL树,牺牲了部分平衡性以在插入、删除操作时减少旋转操作,整体性能优于AVL树
红黑树的插入操作和二叉查找树类似,在正常插入以后,要将插入的结点标为红色,并对树进行类似AVL和SBT的旋转调整,使得树满足红黑树的第(4)条规则,即每个红色结点的子结点都是黑色的。因为如果将结点标记为黑色,会使得第(5)条规则被破坏,很难通过旋转来调整,而通过旋转可以比较容易地使得红色结点分隔开。
当插入之前的树为空时,新结点会位于树的根,根据第(2)条规则,将新结点的颜色改为黑色就可以了。除此之外,当插入的新结点x的父结点是红色的,需要从新结点出发向上进行调整,直到x的父结点为黑色,一共有3种情况:
(1)x的叔父结点是红色的:此时x 的祖父结点一定是黑色的,因此将祖父结点的黑色改为红色,并将x的父结点和叔父结点改为黑色。之后将x的祖父结点作为x继续向上迭代。
(2)x的叔父结点是黑色的,并且x是一个右孩子:对x的祖父结点进行左旋后,x及其父结点仍为红色,叔父结点仍为黑色,需要继续对x执行第(3)种调整。
(3)x 的叔父结点是黑色的,并且x是一个左孩子:此时,将x的父结点改为黑色,祖父结点改为红色,并对x的祖父结点进行右旋。这时,x的父结点为黑色,不需要再向上迭代调整了。
红黑树的删除操作,首先找到要删除的结点,如果这个结点有两个子结点,那么可以把问题转化为删除只有一个子结点的操作。如果两个子结点都是NIL,那么将其中一个NIL当做子结点就可以了。
如果要删除的结点x为红色,那么这个结点的父结点和子结点都一定是黑色,因此,只需要把子结点直接连向父结点就可以了。而如果被删除结点是黑色而其子结点是红色,也只需要把子结点替换上来,并将它的颜色改为黑色就可以了。
接下来所有情况的前提,都是待删除结点及其子结点都为黑色的情况。首先将待删除结点的子结点替换到待删除结点上,标记为x,并将x的兄弟结点标记为w。此时x具有双重黑色,也就是说,在计算路径上的黑色结点时,经过x要计两次数。然后进行以下分情况操作:
(1)x的兄弟结点是红色的:此时对x的父结点进行一次旋转,并改变x的父结点和兄弟结点的颜色。这时,x的新兄弟结点是黑色的,可以按照后面几种情况继续处理。
(2)x的兄弟结点是黑色的,并且w的两个子结点是黑色的:此时去掉x的一重黑色,并将w改为红色,在x的父结点加上额外的一重黑色(如果原来是红色,则改为黑色,否则改为双重黑色)。接下来将x的父结点当做x,继续后续的调整操作。
(3)x的兄弟结点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的:此时交换w和它的左孩子的颜色,然后对w进行右旋操作。现在w的右孩子为红色,可以按照情况(4)进行处理。
(4)x的兄弟结点w是黑色,且w的右孩子是红色的:此时调整w及其父结点和右孩子的颜色,并对w的父结点进行一次左旋,可以使x去掉一重黑色。这时以根结点作为x,继续进行判断。

由于STL中的数据结构set和map是基于红黑树建立的,因此可利用set来简化红黑树的建立过程

// 借用set创建红黑树(二叉查找树)
// 按照前序遍历将树中结点存入set中
template <typename T>
void treeInOrder2Set(Node<T> *root, set<T> &s)
{
    if (!root)
    {
        return;
    }
    treeInOrder2Set(root->lchild, s);
    s.insert(root->data);
    treeInOrder2Set(root->rchild, s);
}
// 将set中的数据按照前序遍历恢复为二叉查找树
template <typename T>
void setInOrder2Tree(set<T> &s, Node<T> *root)
{
    if (!root)
    {
        return;
    }
    setInOrder2Tree(s, root->lchild);
    auto it = s.begin();
    root->data = *it;
    s.erase(it);
    setInOrder2Tree(s, root->rchild);
}
// 借助set将二叉树转换为红黑树式的二叉查找树
void binTree2BinSearchTree(Node<T> *root)
{
    set<T> s;
    // 先按照前序遍历将普通二叉树转换为set
    treeInOrder2Set(root, s);
    // 将set转换为二叉查找树
    setInOrder2Tree(s, root);
}

利用以上函数即可有效实现红黑树的插入新结点、删除结点、查找是否有结点值与给定值相等:
(1)插入新结点:将树转换为set,在set中使用insert()成员函数插入新数据值即可。如果需要查看结果,将set转换为树,对树遍历。
(2)删除结点:将树转换为set,在set中使用find()成员函数找到其所处位置迭代器,使用erase()成员函数删除即可。如果需要查看结果,将set转换为树,对树遍历。
(3)查找值:将树转换为set,在set中使用find()成员函数找到其所处位置迭代器。
(4)如果要查找树中第k小的元素:将树转换为set,定义迭代器it=set.begin(),将迭代器递增k-1次,所得元素即为第k小元素。

其他的二叉查找树还有:SBT(Size Balanced Tree)、伸展树、AA树、treap树和k-d树,但它们的性能并没有比红黑树有更好的提高。

四、多路查找树

多路查找树(multi-way search tree)是每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。由于是查找树,所有元素之间存在某种特定的排序关系。其有4种常见特殊形式:
(1)2-3树;
(2)2-3-4树;
(3)B树;
(4)B+树。
B树(B-tree)是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。结点最大的孩子数目称为B树的阶(order),2-3树是3阶的B树,2-3-4树是4阶的B树
一m阶的B树具有如下性质:
(1)如果根结点不是叶结点,则其至少有2棵子树。
(2)每一个非根的分支结点都有k-1个元素和k个孩子,其中ceil(m/2)≤k≤m。每一个叶子结点n都有k-1个元素。
(3)所有叶子结点都位于同一层次。
(4)所有分支结点包含下列信息数据(n,A0,K1,A1,K2,A2,…,Kn,An),其中Ki(i=1,2,…,n)为关键字,且Ki<K.(i+1)(i=1,2,…,n-1);Ai(i=0,2,…,n)为指向子树根结点的指针,且指针A.(i-1)所指子树中所有结点的关键字均小于Ki(i=1,2,…,n),An所指子树中所有结点的关键字均大于Kn,n(ceil(m/2)-1≤n≤m-1)为关键字的个数(或n+1为子树的个数)。
在B树上查找的过程是一个顺指针查找结点和在结点中查找关键字的交叉过程。
由于B树每结点可以具有比二叉树多得多的元素,所以与二叉树的操作不同,它们减少了必须访问结点和数据块的数量,从而提高了性能。可以说,B树的数据结构就是为内外存的数据交互准备的

B+树是应文件系统所需而发明的一种B树的变形树,从严格意义上讲,其已经不是树结构了。在B树中,每个元素在该树中只出现一次,有可能在叶子结点上,也有可能在分支结点上。而在B+树中,出现在分支结点中的元素会被当作它们在该分支结点位置的中序后继者(叶子结点)中再次列出。另外,每个叶子结点都会保存一个指向后一叶子结点的指针。
一棵m阶的B+树和m阶的B树的差别在于:
(1)有n棵子树的结点中包含有n个关键字;
(2)所有的叶子结点包含全部关键字的信息,及指向含这些关键字记录的指针,叶子结点本身依关键字的大小自小而大顺序链接;
(3)所有分支结点可以看成是索引,结点中仅含有其子树中的最大(或最小)关键字。
该数据结构最大的好处在于,如果我们需要从最小关键字进行从小到大的顺序查找,我们就可以从最左侧的叶子结点出发,不经过分支结点,而是沿着指向下一叶子的指针就可以遍历所有的关键字。
B+树的结构特别适合带有范围的查找

在B+树的基础之上,有一个更为复杂的变种:B树。在B树中,内部结点(非根、非叶子)也增加一个指向兄弟的指针,并且每个结点的关键字个数至少为关键字个数上限的2/3。由于B+树没有对结点内关键字个数的上下限做调整,因此B+树的分裂和B树一致。而由于B内关键字个数的下限有所调整,因此,当一个B树的结点已经满时,如果下一个兄弟结点未满,那么将一部分数据转移到兄弟结点中,再将关键字插入原来的结点;如果兄弟结点也满了,则在原结点与兄弟结点之间增加新结点,原结点和兄弟结点各复制1/3的数据到新的结点,最后在父结点增加到新结点的指针。正因为B*/调整了关键字数量的下限,使得B*树的空间使用率相比B树和B+树更高。

五、并查集

并查集(merge-find set)也被称为不相交集(disjoint set),是由森林构成的一种数据结构,是用于解决若干的不相交集合的几种操作的统称
(1)MAKE-SET(x):初始化操作,建立一个只包含元素x的集合。
(2)UNION(x,y):合并操作,将包含x和y的集合合并为一个新的集合。
(3)FIND-SET(x):查询操作,计算x所在的集合。
并查集这个词通常既可以指代不相交集合的数据结构,也可以表示其对应的算法。
通常并查集初始化操作是对每个元素都建立一个只包含该元素的集合,这意味着每个成员都是自身所在集合的代表,所以只需要将所有成员的父结点设为它自己就好了。
在不相交森林中,并查集的查询操作,指的是查找出指定元素所在有根树的根结点是谁。我们可以通过每个指向父结点的边回溯到结点所在有根树的根,也就是对应集合的代表元素。
并查集的合并操作需要用到查询操作的结果。合并两个元素所在的集合,需要首先求出两个元素所在集合的代表元素,也就是结点所在有根树的根结点。接下来将其中一个根结点的父亲设置为另一个根结点,这样我们就把两棵有根树合并成一棵了。
并查集的查询操作最坏情况下的时间复杂度为O(n)。为了改善时间效率,可以通过启发式合并方法,将包含较少结点的树接到包含较多结点的树根上,可以防止树退化成一条链。另外,我们也可以通过路径压缩的方法来进一步减少均摊复杂度。同时使用这两种优化方法,可以将每次操作的时间复杂度优化至接近常数级。
并查集查询操作的算法流程为:
(1)寻找当前结点的父结点。
(2)如果父结点是它本身,则该父结点为树的根结点,直接返回;否则返回步骤(1)。
并查集合并操作的算法流程为:
(1)分别获得传入的两个结点所在树的根结点。
(2)如果两个根结点相同说明它们在一棵树上,返回false,结束合并操作。
(3)如果两个根结点不同,则将其中一个根结点的父指针指向另一个根结点,合并操作完成,返回true。
如果任选一个根结点使其父结点指向另一个根结点,最后可能导致某一棵树过长甚至退化成链表的情况。我们可以使用按秩合并的启发式策略来解决这个问题。
并查集按秩合并的算法流程为:
(1)利用一个数组保存每个结点的所在树的结点总数,即保存每个结点的秩;
(2)分别获得传入的两个结点所在的树的根结点;
(3)比较两个根结点是否相同,相同则返回false,结束合并操作;
(4)若两个根结点的秩不同,比较它们的秩大小;
(5)将秩较小的根结点的父指针指向秩较大的根结点;
(6)更新合并后的根结点的秩,返回true,结束合并操作。
路径压缩优化也是为了避免树过长以及过多的单链导致查找效率过低的操作。实际上,在进行路径压缩优化时只需在查找根结点时,将待查找的结点的父指针指向它所在的树的根结点就好。

STL中没有并查集的实现,但在boost中有,该并查集实现已经包含了利用秩和路径压缩的优化功能,只需包含头文件即可使用:

#include <boost/pending/disjoint_sets.hpp>

disjoint_sets的模板类定义为:
disjoint_sets<Rank, Parent, FindCompress>
其成员函数包括:
(1)disjoint_sets(Rank r, Parent p),构造函数;
(2)disjoint_sets(const disjoint_sets& x),复制构造函数;
(3)void make_set(Element x),初始化操作,建立一个只包含元素x的集合;
(4)void link(Element x, Element y),合并操作,将由x和y表示的集合合并为一个新的集合;
(5)void union_set(Element x, Element y),合并操作,合并操作,将包含x和y的集合合并为一个新的集合,相当于link(find_set(x), find_set(y));
(6)Element find_set(Element x),查询操作,返回x所在的集合;
(7)std::size_t count_sets(ElementIterator first, ElementIterator last),返回并查集中集合的个数;
(8)void compress_sets(ElementIterator first, ElementIterator last),平化父树,以便每个元素的父元素都是其代表。
boost上的使用示例为:

disjoint_sets<Rank, Parent, FindCompress> dsets(rank, p);
for (ui = vertices(G).first; ui != vertices(G).second; ++ui)
    dsets.make_set(*ui);
...
while (!Q.empty()) {
    e = Q.front();
    Q.pop();
    u = dsets.find_set(source(e));
    v = dsets.find_set(target(e));
    if (u != v) {
       *out++ = e;
       dsets.link(u, v);
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值