当前位置: 首页 > news >正文

数据结构初阶——AVL树的实现(C++)

目录

相关概念

节点的定义

AVL树的插入

AVL树的旋转操作

左旋操作

右旋操作

左右双旋操作

右左双旋

AVL树的验证

AVL树的查找

AVL树的修改

AVL树的删除


相关概念

实际上我们的二叉搜索树的效率是不稳定的,我们可以分析一个极端的情况,那就是如果我们的数据是有序的,我们的二叉搜索树的效率就会退化到单链表的遍历,那样我们的二叉搜索也就不存在了。

所以在1962年苏联数学家G.M. Adelson-Velsky和E.M. Landis提出了一种自平衡的二叉搜索树(BST),他们给出的解决方案是:AVL树可以通过严格的平衡因子的限制,确保树的树的高度在对数的级别,从而在插入、删除和查找操作时,能够有比较优秀的时间复杂度。

总结一下:我们的AVL树有以下几个规则:

1、空树可以是AVL树。

2、树的左右子树是AVL树。

3、树的左右子树的高度差的绝对值不可以超过1,维持在-1/0/1的范围。

给个图:

节点的定义

我们这里的节点设计是使用的三叉链式的结构,因为我们在实现相关功能的时候需要用到向上遍历节点,所以我们这里最好是添加上父节点的指针,除了父节点指针,我们还要有左右孩子指针以及我们的平衡因子。

代码如下:

template <class K, class V>
struct AVLTreeNode {pair<K, V> _kv;AVLTreeNode<K, V>* left;AVLTreeNode<K, V>* right;AVLTreeNode<K, V>* parent;// 平衡因子int _bf;AVLTreeNode(const pair<K, V>& kv): left(nullptr), right(nullptr), parent(nullptr), _kv(kv), _bf(0) {}
};

AVL树的插入

我们AVL树的插入操作有以下几个步骤:

1、找到我们要插入的位置(使用我们的二叉搜索树的相关遍历方法)。

2、插入节点到树里面。

3、插入完成后,我们还需要向上遍历找一找是不是出现的不平衡的情况,并进行旋转操作。

我们这里的插入规则就是我们的二叉搜索树的插入规则,也就是下面的规则:

1、当前节点的key值小于我们的插入节点的key值,向右子树继续到不能继续。

2、当前节点的key值大于我们的插入节点的key值,向左子树继续到不能继续。

3、当前节点的key值等于我们的插入节点的key值,表示已经有值了插入失败。

这里操作之后,我们的cur指正就能指向空节点了,这个时候我们就可以构造我们的节点插入了。

但是我们到了这一步也只是做了一些前置操作,最重要的还是关于调整平衡因子的操作,也就是判断我们插入的节点是不是会影响到我们树中某些节点的平衡因子的大小。

这里我们就需要对新增节点影响的节点进行平衡因子的更新操作,我们这里可能有变化的平衡因子就是我们的插入节点位置的所有祖先(父、祖父、曾祖父、高祖父、天祖父等等等等)。

如图所示:

所以我们总结一个更新的规则:

1、如果新增的节点在父节点的左边,父节点的平衡因子--;

2、如果新增的节点在父节点的右边,父节点的平衡因子++;

敲黑板:

这里的新增节点不一定就是我们的子节点,也可能是孙子、曾孙、玄孙等。

我们更新完了一个节点的平衡因子之后,我们就要判断变化了:

  • 如果是父节点平衡因子等于了0,就是说我们没有必要往上了。
  • 如果是父节点平衡因子等于了-1或是1,我们还是要往上面看看。
  • 如果是父节点平衡因子等于了-2或是2,我们就要进行相应的调整了,也就是我们可能的四种旋转。

敲黑板:

我们二叉树最开始的时候的每一个节点只可能是-1/0/1,否则之前的二叉树就不满足题意了,也就没有了意义。

下面这种情况就是我们最坏的情况了,也就是一路遍历到了到了根节点的情况:

如果我们在更新的时候发现了平衡因子为-2或是2的节点,我们就要进行旋转操作了,首先我们要清楚一点就是我们的parent的平衡因子为-2的时候,我们的cur节点的平衡因子一定是-1或是1,

分析如下:

如果我们的cur的平衡因子是0,那么我们的cur一定就是我们新增加的节点了,不可能是之前一次的parent,因为如果是的话那么上一次就会停止继续我们的向上面更新了;确定了我们的cur就是我们的新增节点后,那么也就是说现在的情况下,我们的父节点的平衡因子更新之后一定是-1/0/1不可能是-2/2(不符合基本条件)。

所以说我们的parent的平衡因子是-2/2的时候,我们的cur的平衡因子一定是-1/1不可能是0的。

接下来就是我们的插入操作的核心流程了,那就是对于旋转的处理,这个操作也是AVL的核心,主要是有下面这四种情况的调整:

1、当我们的父节点是-2的时候,当前节点是-1的时候,进行右旋操作。

2、当我们的父节点是-2的时候,当前节点是1的时候,我们进行左右双旋的操作。

3、当我们的父节点是2的时候,当前节点为-1的时候,我们进行右左双旋操作。

4、当我们的父节点是2的时候,当前节点是1的时候,我们进行左旋操作。

简记:

其实单旋更好判断,就是那边明显多了就往我们相反的反向旋;双旋主要是根据从根节点到当前节点中间的遍历的方向的前后变化来判断,其实如果我们自己实现了一遍也就很简单了,主要还是要足够的熟练(多写)。

代码如下:

bool Insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(kv);if (parent->_kv.first < kv.first){parent->_right = cur;}else{parent->_left = cur;}// 链接父亲cur->_parent = parent;// 控制平衡// 更新平衡因子while (parent){if (cur == parent->_left)parent->_bf--;elseparent->_bf++;if (parent->_bf == 0){break;}else if (parent->_bf == 1 || parent->_bf == -1){cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){if (parent->_bf == -2 && cur->_bf == -1){RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == 1){RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == 1){RotateLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);}else{assert(false);}break;}else{assert(false);}}return true;
}

具体旋转在下面。

AVL树的旋转操作

这里是重点,可能会要求手撕之类的。

左旋操作

示意图:

图示说明:

我们这里用长方形表示可能的子树或是节点或是空节点。

静态示意图:

左旋操作的步骤如下:

1、首先我们要将subR的左子树subRL变成我们parent的右子树。

2、让parent作为我们subR的左子树。

3、让我们的subR作为整个子树的根。

4、更新我们相关节点的平衡因子(parent, SubR)。

左旋过程中是符合二叉树的基本性质的,subR的左子树要比我们的parent大, 所以能够作为我们parent的右子树,我们的parent要比我们的SubR要小,所以可以作为我们SubR的左子树,我们这里所指的都是我们调整之前的值。

平衡因子的更新

代码如下:

void RoateL(Node* parent) {Node* subR = parent->right;Node* subRL = subR->left;parent->right = subRL;if(subRL) {subRL->parent = parent;}Node* pParent = parent->parent;subR->left = parent;parent->parent = subR;if(!pParent) {_root = subR;subR->parent = nullptr;}else {if(parent == pParent->left) {pParent->left = subR;}else {pParent->right = subR;}subR->parent = pParent;}parent->_bf = 0;subR->_bf = 0;
}

右旋操作

静态示意图

我们右旋的步骤如下:

1、将我们的subL的右子树subLR作为我们的parent的左子树。

2、将我们的parent作为我们subL的右子树。

3、我们的subL作为我们当前子树的根(不是整个根节点)。

4、更新相关节点的平衡因子(parent, SubL)。

我们右旋满足我们的二叉搜索树的性质:

1、我们的subL这个子树的节点都要比parent小,所以SubLR可以作为我们parent的左子树。

2、我们的parent要比我们的SubL大,所以可以作为我们SubL的右子树。

平衡因子的更新如下:

我们这里知道其实我们更新的parent和SubL节点的平衡因子都是最终变成了0,这里其实我们从图里面也是能看出来的,但是我们剩下的两种旋转操作就要对节点进行讨论了。

代码如下:

// 右旋
void RoateR(Node* parent) {Node* subL = parent->left;Node* subLR = subL->right;parent->left = subLR;if(subLR) {subLR->parent = parent;}Node* pParent = parent->parent;subL->right = parent;parent->parent = subL;if(!pParent) {_root = subL;subL->parent = nullptr;}else {if(parent == pParent->left) {pParent->left = subL;}else {pParent->right = subL;}subL->parent = pParent;}subL->_bf = subLR->_bf = 0;
}

左右双旋操作

示意图:

静态图:

1、插入一个新节点

2、以我们的subL为节点左旋

3、以我们的parent为节点进行右旋

左右双旋的步骤如下:

1、先让我们的SubL进行左旋。

2、然后再是我们的parent进行右旋。

3、更新我们的平衡因子。

左右双旋后我们的二叉树满足二叉搜索树:

我们观察图示可以看到我们的整个过程的改变,就是让我们的SubLR的左子树作为了我们SubL的右子树,让我们的SubLR的右子树作为了我们parent的左子树,然后我们的SubL作为SubLR的左子树,parent作为SubLR的右子树,我们的SubLR成为我们当前子树的根,具体的大小关系:

1、SubLR整体比我们的SubL要小,所以它的左子树可以作为我们SubL的右子树。

2、我们的SubLR的右子树要比我们的parent要小,所以可以作为parent的左子树。

3、我们SubL及其左子树加上新来的节点都是要比SubLR要小的,可以作为其左子树;我们的parent及其右子树加上新来的节点都是要比我们的SubLR要大的,可以作为其右子树。

旋转过后,我们就要进行我们的平衡因子的更新了,根据我们的SubLR平衡因子的不同,这里的更新我们有三种情况:

1、当我们的SubLR的平衡因子是-1的时候,双旋操作后我们的SubL(0),SubLR(0),parent(1)。

2、当我们的SubLR的平衡因子是1的时候,双旋操作后我们的SubL(-1),parent(0),SubLR(0)。

3、当我们的SubR的平衡因子是0的时候,我们的左右双旋操作后我们的parent(0),subL(0),SubLR(0)。

代码如下:

// 左右双旋
void RoateLR(Node* parent) {Node* subL = parent->left;Node* subLR = parent->right;int bf = subLR->_bf;RoateL(parent->left);RoateR(parent);if(bf == -1) {subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else if(bf == 1) {subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if(bf == 0) {subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}else {assert(false);}
}

右左双旋

示意图

静态示意图

1、插入一个节点

2、以我们的SubR为节点左旋

3、以我们的parent有旋

右左双旋的步骤如下:

1、我们要以SubR为点右旋。

2、以parent为点左旋。

3、更新我们的平衡因子。

右左双旋后我们的二叉树满足的性质:

右左双旋的整体效果就是将我们的SubRL的左子树作为我们parent的右子树,将我们的SubRL的右子树作为我们SubR的左子树,分析如下:

1、SubRL的左子树要比我们的parent的值要大,所以可以作为我们parent的右子树。

2、SubRL的右子树要比我们的SubR的值大,所以可以作为SubR的左子树。

3、我们的parent加上我们新加的节点要比我们的SubRL小,我们的SubR加上我们的新来的节点要比我们的SubRL要大所以可以作为其右子树。

右左双旋中间平衡因子的变化也是根据我们的SubLR来变化的,分为下面三种情况:

1、当我们的SubRL的平衡因子是1时,双旋之后我们的平衡因子更新parent(-1),SubR(0),SubRL(0)。

2、当我们的SubRL的平衡因子是-1时,双旋之后我们的平衡因子更新parent(0),SubR(1),SubRL(0)。

3、当我们的SubRL的平衡因子是0时,双旋之后我们的平衡因子更新parent(0),SubR(0),SubRL(0)。

代码如下:

// 右左双旋
void RoateRL(Node* parent) {Node* subR = parent->right;Node* subRL = subR->left;int bf = subRL->_bf;RoateR(parent->right);RoateL(parent);if(bf == -1) {subRL->_bf = 0;subR->bf = 1;parent->bf = 0;}else if(bf == 1) {subRL->_bf = 0;subR->bf = 0;parent->bf = -1;}else if(bf == 0){subRL->_bf = 0;subR->bf = 0;parent->bf = 0;}else {assert(false);}
}

AVL树的验证

我们这里AVL树的验证主要是两方面的验证,一个是我们基础的二叉搜索树的验证,一个是我们的平衡性的验证,我们先来验证我们的二叉搜索树:

代码如下:

// 实现函数:中序遍历
void _InOrder(Node* root) {if(root == nullptr) {return;}_InOrder(root->left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->right);
}
// 外部调用函数
void Inorder() {_InOrder(_root);
}

我们这里是为了证明这是一个二叉树搜索树,我们还需要正明这个树的平衡性,也就是遍历节点中的平衡因子是不是符合我们的要求,

我们这里采用先序遍历的方法,步骤如下:

1、先计算当前节点左右子树的高度,判断是不是符合条件的。

2、递归左子树,递归右子树,返回的结果就是左右子树是不是都是平衡的。

3、递归出口就是我们的空树,空树是AVL树。

敲黑板:

这里的实现有很多种,大家可以选取一个比较对口的方法实现,比如使用后序遍历也是可以的。

代码如下:

// 对外接口
bool IsAVLTree() {return _IsBalanceTree(_root);
}
// 判断平衡性
bool _IsBalanceTree(Node* root) {// 空树也是AVL树if(root == nullptr) {return true;}int leftHeight = _Height(root->left);int rightHeight = _Height(root->right);int diff = rightHeight - leftHeight;if(abs(diff) >= 2) {cout << "这节点的高度差异常:" << root->_kv.first << endl;return false;}if(root->_bf != diff) {cout << "这个节点的平衡因子异常:" << root->_kv.first << endl;return false; }return _IsBalanceTree(root->left) && _IsBalanceTree(root->right);
}

AVL树的查找

这里的查找逻辑实际上就是我们的二叉搜索树的查找逻辑,步骤如下:

1、判断空树(注意),是就返回nullptr。

2、如果我们的key值要小于我们当前节点的值,我们就在我们的左子树中查找。

3、如果我们的key值要大于我们当前节点的值,我们就在我们的右子树中查找。

4、如果我们的key值等于我们当前节点的值,查找成功返回对应的节点。

// 查找函数
Node* Find(const K& key) {Node* cur = _root;while(cur) {if(key < cur->_kv.first) {cur = cur->left;}else if(key > cur->_kv.first) {cur = cur->right;}else {return cur;}}return nullptr;
}

AVL树的修改

实现方式一:

我们可以找到指定的节点进行修改即可,实现逻辑如下:

1、调用我们上面实现的查找函数,找到对应的节点。

2、对这个节点的value进行修改。

代码如下:

bool Modify(const K& key, const V& value) {Node* pos = Find(key);if(pos == nullptr) {return false;}pos->_kv.second = value;return true;
}

实现方式二:

我们一般的在使用这样的修改函数的时候,一般是和我们之前实现的map是一样的,这里我们首先要重写我们对的插入函数,返回值我们统一为pair类型(第一个是节点的指针,第二个是返回的bool值),那就是下面的逻辑:

1、如果我们要插入的key值没有的时候,我们就插入这个节点,返回我们的节点指针和true。

2、如果我们要插入的key值已经存在了,我们就插入失败了,返回我们key值对应的节点指针和false。

也就是对我们之前实现的插入函数进行一些修改即可,代码如下
 

pair<Node*, bool> Insert(const pair<K, V>& kv) {if(_root == nullptr) {_root = new Node(kv);return make_pair(_root, true);}Node* cur = _root;Node* parent = nullptr;while(cur) {if(cur->_kv.first > kv.first) {cur = cur->left;parent = cur;}else if(cur->_kv.first < kv.first) {cur = cur->right;parent = cur;}else {return make_pair(cur, false);}}cur = new Node(kv);Node* temp = cur;if(parent->_kv.first > kv.first) {parent->left = cur;}else {parent->right = cur;}cur->parent = parent;while(cur != _root) {if(cur == parent->left) {parent->_bf--;}else if(cur == parent->right) {parent->_bf++;}if(parent->_bf == 0) {break;}else if(parent->_bf == -1 || parent->_bf == 1){cur = parent;parent = parent->parent;}else if(parent->_bf == -2 || parent->_bf == 2){if(parent->_bf == -2) {if(cur->_bf == -1) {RoateR(parent);} else {RoateLR(parent);}}else if(parent->_bf == 2){if(cur->_bf == 1) {RoateL(parent);}else {RoateRL(parent);}}break;}else {assert(false);}return make_pair(temp, true);} 
}

然后我们就可以对我们的运算符[ ]进行重载了,重载的逻辑如下:

1、调用我们上面改好了的插入函数插入我们的键值对。

2、拿出插入函数获取到的节点。

3、返回我们节点value的引用。

这样操作之后,我们在使用[key]的时候,逻辑如下:

  • 当我们的key不在树里面的时候,就插入我们的键值对<key, V()>,然后返回该键值对中value的引用即可。
  • 当我们的key已经在树中的时候,我们就返回键值key对应节点的value的引用。

代码如下:

V &operator[](const K &key) {pair<Node *, bool> pos = Insert(make_pair(key, V()));Node *node = pos.first;return node->_kv.second;
}

AVL树的删除

我们这里进行的删除操作还是比较麻烦的,平衡二叉搜索树的删除操作好像都挺复杂的,难度可以算是洛谷绿题的水平了,实现的步骤如下:

1、找到我们要删除的节点,没有就返回false。

2、找到的待删除的节点的左右子树都不是空的,那么我们这个时候就要进行替换删除了,因为这个时候的删除调整要考虑到我们的子树里面去。

3、根据删除操作讨论平衡因子的更新操作。

4、删除该节点。

我们这里的替换法删除有两种实现的方式(这个过程类似于狸猫换太子):

  • 第一种就是我们找到待删除节点左子树中key值最大的节点替换成待删除的节点被删除,因为我们待删除的节点就是相对于左子树中节点是最大的,删除了它就要重新来一个最大的,最后将我们的待删除的节点继承上我们的删除节点的值;
  • 第二种就是我们找到待删除节点的右子树中key最小的一个替换待删除节点被删除,因为我们的待删除节点相对于我们的右子树中的节点是最小的,删除了它我们要一个最小的来维持二叉搜索树的性质,最后我们的待删除节点继承了删除节点的值。

我们这里实际被删除的节点的左右子树中至少要有一个空树。

我们在删除之前,需要先进行我们的平衡因子的更新操作,删除后更新比较麻烦,我们这里的更新规则如下:

  • 1、删除节点在parent的右边,那么我们的parent的平衡因子就要减减。
  • 2、删除节点在parent的左边,那么我们的parent的平衡因子就要加加。

更新完了之后,我们还要进行后续往上遍历的判断和我们的插入类似:

1、如果我们的parent的平衡因子等于-1或是1,表明了我们就不需要继续往上面更新平衡因子了。(这里的情况和我们插入操作不一样,因为这个平衡因子是根据0变化来的,所以是不用向上面更新的,因为对于上一个节点的平衡因子没有影响)

2、如果我们的parent的平衡因子等于0,表明我们还需要继续往上更新。(我们这里的情况也是和我们的插入操作不一样的,因为这个状态可能是1或是-1变化来的,也就对我们的上面节点产生了影响)。

3、如果我们的parent的平衡因子等于-2或是2,表明这个时候我们的平衡因子的不正常了,就要进行旋转来调节平衡了。

我们更新完了我们的平衡因子之后,我们就要进行我们的节点删除了,我们知道我们删除的节点的左右子树有一个子树是空的,所以我们实际删除的逻辑如下:

1、如果我们实际删除的节点的左子树是空的,那么就让我们的实际删除节点的右子树链接到我们的parent上面。

2、如果我们实际删除的节点的右子树是空的,那么就让我们的实际删除节点的左子树链接到我们的parent上面。

3、删除我们的节点。

我们进行旋转操作的时候有下面6中情况:

1、parent的平衡因子是-2,parent的左孩子的平衡因子是-1,进行右旋。

2、parent的平衡因子是-2,parent的左孩子的平衡因子是1,进行左右双旋。

3、parent的平衡因子是-2,parent的左孩子的平衡因子是0,我们这里还是要进行右旋的操作,但是有平衡因子调整上的区别。

4、parent的平衡因子是2,parent的右孩子的平衡因子是-1,进行右左双旋。

5、parent的平衡因子是2,parent的右孩子的平衡因子是1,进行左旋。

6、parent的平衡因子是2,parent的右孩子的平衡因子是0,进行左旋,但是平衡因子上面有不同的调整。

敲黑板:

我们这里的三和六是不用再进行向上面调整的,但是其他都要,可以画图理解,从图中不难得出我们的三和六情况的树的高度是没有变化的。

代码如下:

bool Erase(const K &key) {Node *parent = nullptr;Node *cur = _root;Node *deleteParentNode = nullptr;Node *deleteNode = nullptr;while (cur) {if (key > cur->_kv.first) {parent = cur;cur = cur->right;} else if (key < cur->_kv.first) {parent = cur;cur = cur->left;} else {if (cur->left == nullptr) // 左子树为空{if (cur == _root) {_root = _root->right;if (_root) {_root->parent = nullptr;}delete cur;return true;} else {deleteParentNode = parent;deleteNode = cur;}break;} else if (cur->right == nullptr) // 右子树为空{if (cur == _root) {_root = _root->left;if (_root) {_root->parent = nullptr;}delete cur;return true;} else {deleteParentNode = parent;deleteNode = cur;}break;} else // 左右子树都不是空,要进行替换删除{// 找到我们删除节点的右子树中最小的作为提换Node *minParent = cur;Node *minRight = cur->right;while (minRight->left) {minParent = minRight;minRight = minRight->left;}// 替换里面的内容cur->_kv.first = minRight->_kv.first;cur->_kv.second = minRight->_kv.second;// 替换删除的节点deleteParentNode = minParent;deleteNode = minRight;break;}}if (deleteParentNode == nullptr) {return false;}// 记录节点Node* dNode = deleteNode;Node* dpNode = deleteParentNode;// 更新平衡因子while (deleteNode != _root) {if (deleteNode == deleteParentNode->left) {deleteParentNode->_bf++;} else if (deleteParentNode == deleteParentNode->right) {deleteParentNode->_bf--;}if (deleteParentNode->_bf == 0) {deleteNode = deleteParentNode;deleteParentNode = deleteParentNode->parent;} else if (deleteParentNode->_bf == -1 || deleteParentNode->_bf == 1) {break;} else if (deleteParentNode->_bf == -2 || deleteParentNode->_bf == 2) {if (deleteParentNode->_bf == -2) {if (deleteParentNode->left->_bf == -1) {Node *temp = deleteParentNode->left;RoateR(deleteParentNode);deleteParentNode = temp;} else if (deleteParentNode->left->_bf == 1) {Node *temp = deleteParentNode->left->right;RoateLR(deleteParentNode);deleteParentNode = temp;} else {Node *temp = deleteParentNode->left;RoateR(deleteParentNode);deleteParentNode->_bf = 1;deleteParentNode->right->_bf = -1;break;}} else {if (delParentPos->_right->_bf == -1) {Node *tmp = delParentPos->_right->_left; RotateRL(delParentPos); delParentPos = tmp;     } else if (delParentPos->_right->_bf == 1) {Node *tmp = delParentPos->_right; RotateL(delParentPos); delParentPos = tmp;   } else                   {Node *tmp = delParentPos->_right; RotateL(delParentPos);delParentPos = tmp;    delParentPos->_bf = -1;delParentPos->_left->_bf = 1;break; }}deleteNode = deleteParentNode;deleteParentNode = deleteParentNode->parent;}else {assert(false);}}// 进行删除if(dNode->left == nullptr){if(dNode == dpNode->left) {dpNode->left = dNode->right;if(dNode->right) {dNode->right->parent = dpNode;}}else {dpNode->right = dNode->right;if(dNode->right) {dNode->right->parent = dpNode;}}}else {if(dNode == dpNode->left) {dpNode->left = dNode->left;if(dNode->left) {dNode->left->parent = dpNode;}}else {dpNode->right = dNode->left;if(dNode->left) {dNode->left->parent = dpNode;}}}delete dNode;return true;}
http://www.dtcms.com/a/400038.html

相关文章:

  • 【计算机通识】IoT 是什么、如何工作、关键技术、应用场景、挑战与趋势
  • php网站开发手机绑定谷歌推广外包
  • 如何面试网站开发学校网站建设与管理
  • 青岛网站建设咨询wordpress head.php
  • 企业网站管理系统网页制作模板的网站免费
  • 【Shell】Shell脚本基础知识
  • 北京seo推广清远网站推广优化公司
  • 基于STM32单片机远程浇花花盆GSM短信浇水补光设计
  • 丘里奇网站排名黑龙江省建设教育协会网站
  • 【汽车篇】AI深度学习在汽车焊缝3D视觉检测的应用
  • 上海专业高端网站建佳城建站 网站
  • RNN 与 LSTM:解密序列数据的 “记忆大师”
  • 【力扣LeetCode】 1422_分割字符串的最大得分
  • 东莞企业网站推广公司可以看任何网站的浏览器
  • SQL,使用递归 CTE 删除层级菜单项
  • wordpress做英文站怎样做一个网站
  • 泰安本地网站南宁网站建设公司比优建站
  • 做视频上什么网站找创意青岛福瀛建设集团有限公司网站
  • 工程公司网站模板下载电子商务的网站设计
  • Linux安装tomcat
  • 02、Python从入门到癫狂:函数与数据容器
  • 新能源知识库(107)什么是欧盟的电池新规?
  • 杭州做网站优化广州注册公司在哪个网站
  • Docker 日志管理与备份实践文档
  • 做网站猫腻大吗wordpress 设置cookie
  • 网站提示风险可以做微积分的网站
  • 清河网站建设设计费用自媒体平台收益
  • 斯坦福Percy Liang团队:如果有足够显卡,如何设计最佳预训练策略?
  • 旅游网站设计图片内蒙古网上办事大厅官网
  • Tomcat服务器指南