Skip to content

C++实现红黑树(插入、查找、删除) #6

Description

@wellwei

C++实现红黑树(插入、查找、删除)

红黑树性质

  • 所有节点都是红或黑色
  • 根节点是黑色
  • 每个叶子节点(NIL节点,或NULL)都是黑色
  • 红色节点的两个子节点都是黑色(不会出现相邻的两个红色节点)。不红红
  • 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,包含相同数量的黑色节点。黑路同

以上特性保证了最长路径不会超过最短路径的两倍。保证了树的平衡性,由此插入和删除操作的复杂度为O(log n)

本文建议搭配该网页演示理解

定义结点结构

首先定义红黑树的节点结构。每个节点包含数据、指向父节点和左右子节点的指针,以及一个表示节点颜色的属性(红色或黑色)。

template <typename T>
struct Node {
    T data;
    Node<T> *parent, *left, *right;
    bool isRed;
    
    Node(const T& data) : data(data), parent(nullptr), left(nullptr), right(nullptr), isRed(true) {}
};

在这个结构体中:

  • data:存储节点的数据。我们使用了模板 typename T,使得红黑树可以存储不同类型的数据。
  • parent:指向父节点的指针。
  • left:指向左子节点的指针。
  • right:指向右子节点的指针。
  • isRed:表示节点的颜色,true为红色,false为黑色。新插入的节点通常被染成红色。

定义红黑树类

定义红黑树类,包含根节点、NIL节点和一些基本操作。

template <typename T>
class RBTree {
private:
	Node<T> *root;
	Node<T> *nil;

public:
	RBTree() {
		nil = new Node<T>(0);
		nil->isRed = false;
		nil->parent = nil->left = nil->right = nullptr;
		root = nil;
	}

	// 插入
	void insert(const T&);

	// 删除
	void erase(const T&);

	// 查找
	Node<T>* find(const T&);

	// 左旋
	void rotateLeft(Node<T>*);

	// 右旋
	void rotateRight(Node<T>*);

	// 插入后修复红黑树性质
	void insertFixUp(Node<T>*);

	// 删除后修复红黑树性质
	void eraseFixUp(Node<T>*);

	// 查找最小节点
	Node<T>* findMin(Node<T>*);

	// 查找最大节点
	Node<T>* findMax(Node<T>*);

	// 查找前驱
	Node<T>* predecessor(Node<T>*);

	// 查找后继
	Node<T>* successor(Node<T>*);
    
    // 替换子树
	void transplant(Node<T> *u, Node<T> *v);
};

声明一些核心操作以及一些用于维护红黑树性质的辅助函数。

nil节点为哨兵节点,用来代替nullptr,防止空指针操作异常。

实现查找操作

红黑树的查找操作与一般的二叉搜索树的查找相同。

template <typename T>
Node<T> *RBTree<T>::find(const T &data) const {
	Node<T> *cur = root;
	while (cur != nil) {
		if (cur->data == data)
			return cur;
		else if (cur->data < data)
			cur = cur->right;
		else
			cur = cur->left;
	}
	return nullptr;
}

实现左旋和右旋

旋转操作是红黑树保持平衡的关键,与平衡二叉树(AVL)的旋转操作类似。

左旋:原根右子节点成为新根,如果新根存在左子节点,那么将其作为原根的右子节点,原根变为新根的左子节点。

右旋:与左旋相反,原根左子节点成为新根,如果新根存在右子节点,那么将其作为原根的左子节点,原根变为新根的右子节点。

template <typename T>
void RBTree<T>::rotateLeft(Node<T> *node) {
	Node<T> *newNode = node->right;	// 原根的右孩子成为新根
	node->right = newNode->left;	// 新根的左孩子变为旧根的右孩子
	if (newNode->left != nil)
		newNode->left->parent = node;	// 更新左孩子的父节点为旧根
	newNode->parent = node->parent;	// 更新新根的父节点为旧根的父节点
	if (node->parent == nil)
		root = newNode;	// 如果旧根是根节点,更新根节点为新根
	else if (node == node->parent->left)
		node->parent->left = newNode;	// 如果旧根是父节点的左孩子,更新父节点的左孩子为新根
	else
		node->parent->right = newNode;	// 如果旧根是父节点的右孩子,更新父节点的右孩子为新根
	newNode->left = node;	// 旧根变为新根的左孩子
	node->parent = newNode;	// 旧根的父节点变为新根
}

template <typename T>
void RBTree<T>::rotateRight(Node<T> *node) {
	Node<T> *newNode = node->left;
	node->left = newNode->right;
	if (newNode->right != nil)
		newNode->right->parent = node;
	newNode->parent = node->parent;
	if (node->parent == nil)
		root = newNode;
	else if (node == node->parent->left)
		node->parent->left = newNode;
	else
		node->parent->right = newNode;
	newNode->right = node;
	node->parent = newNode;
}

实现插入操作

红黑树的插入操作是标准的二叉搜索树插入,然后通过insertFixUp函数维护红黑树的性质。每个新插入的节点都是红色。

template <typename T>
void RBTree<T>::insert(const T &data) {
	Node<T> *newNode = new Node<T>(data);
	Node<T> *cur = root, *pre = nil;
	while (cur != nil) {
		pre = cur;
		if (cur->data == data) {	// 重复数据不插入
			delete newNode;
			return;
		} else if (cur->data < data)
			cur = cur->right;
		else
			cur = cur->left;
	}
	newNode->parent = pre;

	if (pre == nil)	// 空树
		root = newNode;
	else if (pre->data < data)
		pre->right = newNode;
	else
		pre->left = newNode;

	newNode->left = nil;
	newNode->right = nil;
	newNode->isRed = true;
	
	insertFixUp(newNode);
}

实现插入后的修复操作 insertFixUp

插入操作如果破坏了红黑树的性质,那么就要按照以下变色旋转规则修复:

  1. 当前节点(红色)的父节点是红色且当前节点的叔叔节点也是红色
    1. 把父节点设置为黑色
    2. 把叔叔节点也设置为黑色
    3. 把祖父节点设置为红色
    4. 上移至祖父节点继续修复
  2. 当前节点的父节点是红色,叔叔节点不存在或叔叔节点为黑色,且父节点是祖父节点的左子节点
    • LL型(当前节点是父节点的左子节点):父节点变黑,祖父节点变红,右旋祖父节点
    • LR型(当前节点是父节点的右子节点):左旋父节点变成LL型
  3. 当前节点的父节点是红色,叔叔节点不存在或叔叔节点为黑色,且父节点是祖父节点的右子节点

    • RR型(当前节点是父节点的右子节点):父节点变黑,祖父节点变红,左旋祖父节点
    • RL型(当前节点是父节点的左子节点):右旋父节点变成RR型
template <typename T>
void RBTree<T>::insertFixUp(Node<T> *node) {
	while (node->parent->isRed) {	// 当前节点与父节点都是红,违反 “不红红”。
		if (node->parent == node->parent->parent->left) { // 父节点是祖父节点的左子节点
			Node<T> *uncle = node->parent->parent->right;
			if (uncle->isRed) { // 叔叔节点是红色
				node->parent->isRed = false;
				uncle->isRed = false;
				node->parent->parent->isRed = true;
				node = node->parent->parent; // 上移至祖父节点继续修复
			} else {
				if (node == node->parent->right) { // LR型,先左旋转为LL型
					node = node->parent;
					rotateLeft(node);
				}
				node->parent->isRed = false; // LL型,父节点变黑、祖父节点变红,右旋转祖父节点
				node->parent->parent->isRed = true;
				rotateRight(node->parent->parent);
			}
		} else { // 父节点是祖父节点的右子节点
			Node<T> *uncle = node->parent->parent->left;
			if (uncle->isRed) {
				node->parent->isRed = false;
				uncle->isRed = false;
				node->parent->parent->isRed = true;
				node = node->parent->parent;
			} else {
				if (node == node->parent->left) { // RL型,先右旋转为RR型
					node = node->parent;
					rotateRight(node);
				}
				node->parent->isRed = false; // RR型,父节点变黑、祖父节点变红,左旋转祖父节点
				node->parent->parent->isRed = true;
				rotateLeft(node->parent->parent);
			}
		}
	}

	root->isRed = false; // 根节点始终为黑
}

实现删除操作的辅助函数*

实现查找最小、最大、后继、前驱节点以及替换子树函数。这些辅助函数再删除操作中会用到。

template <typename T>
Node<T> *RBTree<T>::findMin(Node<T> *node) const {
	while (node != nil && node->left != nil) {
		node = node->left;
	}
	return node;
}

template <typename T>
Node<T> *RBTree<T>::findMax(Node<T> *node) const {
	while (node != nil && node->right != nil) {
		node = node->right;
	}
	return node;
}

template <typename T>
Node<T> *RBTree<T>::successor(Node<T> *node) const {
	if (node->right != nil) {
		return minimum(node->right);
	}
	Node<T> *y = node->parent;
	while (y != nil && node == y->right) {
		node = y;
		y = y->parent;
	}
	return y;
}

template <typename T>
Node<T> *RBTree<T>::predecessor(Node<T> *node) const {
	if (node->left != nil) {
		return maximum(node->left);
	}
	Node<T> *y = node->parent;
	while (y != nil && node == y->left) {
		node = y;
		y = y->parent;
	}
	return y;
}

template <typename T>
void RBTree<T>::transplant(Node<T>* u, Node<T>* v) {
    if (u->parent == nullptr) {
        root = v;
    } else if (u == u->parent->left) {
        u->parent->left = v;
    } else {
        u->parent->right = v;
    }
    if (v != nullptr) {
        v->parent = u->parent;
    }
}

实现删除操作

删除操作也需要先执行标准的二叉搜索树删除,然后通过 deleteFixUp 函数来维护红黑树的性质。

template <typename T>
void RBTree<T>::erase(const T &data) {
	Node<T> *node = find(data);
	if (node == nullptr)
		return;

	Node<T> *x, *y = node;
	bool yOriginalColor = y->isRed;

	if (node->left == nil) {	// 左子树为空,用右子树替代
		x = node->right;
		transplant(node, node->right);
	} else if (node->right == nil) {	// 右子树为空,用左子树替代
		x = node->left;
		transplant(node, node->left);
	} else {	// 左右子树均不为空,找到后继节点
		y = findMin(node->right);
		yOriginalColor = y->isRed;
		x = y->right;
		if (y->parent == node) {	// 后继节点是待删除节点的右子节点,直接用右子节点替代
			if (x != nil)
				x->parent = y;
		} else {	// 用后继节点的右子节点替代后继节点
			transplant(y, y->right);
			y->right = node->right;
			y->right->parent = y;
		}
		transplant(node, y);	// 用后继节点替代待删除节点
		y->left = node->left;
		y->left->parent = y;
		y->isRed = node->isRed;
	}
	if (!yOriginalColor) {	// 删除节点为黑色,修复红黑树性质
		eraseFixUp(x);
	}
	delete node;
}

实现删除后的修复操作eraseFixUp

如果删除后破坏了红黑树的性质,那么需要根据以下规则进行修复:

  • 当前节点是左子节点
    • 兄弟节点是红色:父变红、兄变黑、左旋父节点,继续修复
    • 兄弟节点是黑色:
      • 兄弟的两个子节点都是黑色:兄变红,上移继续修复
      • 兄弟节点至少有一个红色子节点:
        • RL型:兄弟节点的右子节点是黑色,兄弟节点变红,其左子节点变黑,右旋兄弟节点变成RR型
        • RR型:父节点颜色给兄弟节点,父节点变黑,右子变黑,左旋父节点
  • 当前节点是右子节点
    • 兄弟节点是红色:父变红、兄变黑、右旋父节点,继续修复
    • 兄弟节点是黑色:
      • 兄弟的两个子节点都是黑色:兄变红,上移继续修复
      • 兄弟至少有一个红色子节点:
        • LR型:兄弟节点的左子节点是黑色,兄弟节点变红,其右子节点变黑,左旋兄弟节点变成RR型
        • LL型:父节点颜色给兄弟节点,父节点变黑,左子变黑,右旋父节点
template <typename T>
void RBTree<T>::eraseFixUp(Node<T> *node) {
	while (node != root && !node->isRed) { // 当前节点不是根节点且不是红色
		if (node == node->parent->left) {  // 当前节点是左子节点
			Node<T> *bro = node->parent->right;
			if (bro->isRed) { // 兄弟节点是红色, 父节点变红,兄弟节点变黑,左旋父节点,继续修复
				bro->isRed = false;
				node->parent->isRed = true;
				rotateLeft(node->parent);
				bro = node->parent->right;
			}
			if (!bro->left->isRed && !bro->right->isRed) { // 兄弟节点的两个子节点都是黑色,兄弟节点变红,上移继续修复
				bro->isRed = true;
				node = node->parent;
			} else { // 兄弟节点至少有一个红色子节点
				// 兄弟节点的右子节点是黑色,左子节点是红色,RL型,兄弟节点变红,其左子节点变黑,右旋兄弟节点变成RR型
				if (!bro->right->isRed) {
					bro->left->isRed = false;
					bro->isRed = true;
					rotateRight(bro);
					bro = node->parent->right;
				}
				// RR型,兄弟节点的右子节点是红色,父节点颜色给兄弟节点,父节点变黑,右子变黑,左旋父节点
				bro->isRed = node->parent->isRed;
				node->parent->isRed = false;
				bro->right->isRed = false;
				rotateLeft(node->parent);
				node = root;
			}
		} else {
			Node<T> *bro = node->parent->left;
			if (bro->isRed) {
				bro->isRed = false;
				node->parent->isRed = true;
				rotateRight(node->parent);
				bro = node->parent->left;
			}
			if (!bro->right->isRed && !bro->left->isRed) {
				bro->isRed = true;
				node = node->parent;
			} else {
				// LR型,兄弟节点的左子节点是黑色,兄弟节点变红,其右子节点变黑,左旋兄弟节点变成RR型
				if (!bro->left->isRed) {
					bro->right->isRed = false;
					bro->isRed = true;
					rotateLeft(bro);
					bro = node->parent->left;
				}
				// RR型,兄弟节点的左子节点是红色,父节点颜色给兄弟节点,父节点变黑,左子变黑,右旋父节点
				bro->isRed = node->parent->isRed;
				node->parent->isRed = false;
				bro->left->isRed = false;
				rotateRight(node->parent);
				node = root;
			}
		}
	}
	node->isRed = false;
}

总结&测试

template <typename T>
struct Node {
	T data;
	Node<T> *parent, *left, *right;
	bool isRed;

	Node(const T &data) : data(data), parent(nullptr), left(nullptr), right(nullptr), isRed(true) {}
};

template <typename T>
class RBTree {
private:
	Node<T> *root;
	Node<T> *nil;

public:
	RBTree() {
		nil = new Node<T>(0);
		nil->isRed = false;
		nil->parent = nil->left = nil->right = nullptr;
		root = nil;
	}

	// 插入
	void insert(const T &);

	// 删除
	void erase(const T &);

	// 查找
	Node<T> *find(const T &) const;

	// 左旋
	void rotateLeft(Node<T> *);

	// 右旋
	void rotateRight(Node<T> *);

	// 插入后修复红黑树性质
	void insertFixUp(Node<T> *);

	// 删除后修复红黑树性质
	void eraseFixUp(Node<T> *);

	// 查找最小节点
	Node<T> *findMin(Node<T> *) const;

	// 查找最大节点
	Node<T> *findMax(Node<T> *) const;

	// 查找前驱
	Node<T> *predecessor(Node<T> *) const;

	void transplant(Node<T> *u, Node<T> *v);

	// 查找后继
	Node<T> *successor(Node<T> *) const;
};

template <typename T>
Node<T> *RBTree<T>::find(const T &data) const {
	Node<T> *cur = root;
	while (cur != nil) {
		if (cur->data == data)
			return cur;
		else if (cur->data < data)
			cur = cur->right;
		else
			cur = cur->left;
	}
	return nullptr;
}

template <typename T>
void RBTree<T>::rotateLeft(Node<T> *node) {
	Node<T> *newNode = node->right; // 原根的右孩子成为新根
	node->right = newNode->left;	// 新根的左孩子变为旧根的右孩子
	if (newNode->left != nil)
		newNode->left->parent = node; // 更新左孩子的父节点为旧根
	newNode->parent = node->parent;	  // 更新新根的父节点为旧根的父节点
	if (node->parent == nil)
		root = newNode; // 如果旧根是根节点,更新根节点为新根
	else if (node == node->parent->left)
		node->parent->left = newNode; // 如果旧根是父节点的左孩子,更新父节点的左孩子为新根
	else
		node->parent->right = newNode; // 如果旧根是父节点的右孩子,更新父节点的右孩子为新根
	newNode->left = node;			   // 旧根变为新根的左孩子
	node->parent = newNode;			   // 旧根的父节点变为新根
}

template <typename T>
void RBTree<T>::rotateRight(Node<T> *node) {
	Node<T> *newNode = node->left;
	node->left = newNode->right;
	if (newNode->right != nil)
		newNode->right->parent = node;
	newNode->parent = node->parent;
	if (node->parent == nil)
		root = newNode;
	else if (node == node->parent->left)
		node->parent->left = newNode;
	else
		node->parent->right = newNode;
	newNode->right = node;
	node->parent = newNode;
}

template <typename T>
void RBTree<T>::insert(const T &data) {
	Node<T> *newNode = new Node<T>(data);
	Node<T> *cur = root, *pre = nil;
	while (cur != nil) {
		pre = cur;
		if (cur->data == data) { // 重复数据不插入
			delete newNode;
			return;
		} else if (cur->data < data)
			cur = cur->right;
		else
			cur = cur->left;
	}
	newNode->parent = pre;

	if (pre == nil) // 空树
		root = newNode;
	else if (pre->data < data)
		pre->right = newNode;
	else
		pre->left = newNode;

	newNode->left = nil;
	newNode->right = nil;
	newNode->isRed = true;

	insertFixUp(newNode);
}

template <typename T>
void RBTree<T>::insertFixUp(Node<T> *node) {
	while (node->parent->isRed) {						  // 当前节点与父节点都是红,违反 “不红红”。
		if (node->parent == node->parent->parent->left) { // 父节点是祖父节点的左子节点
			Node<T> *uncle = node->parent->parent->right;
			if (uncle->isRed) { // 叔叔节点是红色
				node->parent->isRed = false;
				uncle->isRed = false;
				node->parent->parent->isRed = true;
				node = node->parent->parent; // 上移至祖父节点继续修复
			} else {
				if (node == node->parent->right) { // LR型,先左旋转为LL型
					node = node->parent;
					rotateLeft(node);
				}
				node->parent->isRed = false; // LL型,父节点变黑、祖父节点变红,右旋转祖父节点
				node->parent->parent->isRed = true;
				rotateRight(node->parent->parent);
			}
		} else { // 父节点是祖父节点的右子节点
			Node<T> *uncle = node->parent->parent->left;
			if (uncle->isRed) {
				node->parent->isRed = false;
				uncle->isRed = false;
				node->parent->parent->isRed = true;
				node = node->parent->parent;
			} else {
				if (node == node->parent->left) { // RL型,先右旋转为RR型
					node = node->parent;
					rotateRight(node);
				}
				node->parent->isRed = false; // RR型,父节点变黑、祖父节点变红,左旋转祖父节点
				node->parent->parent->isRed = true;
				rotateLeft(node->parent->parent);
			}
		}
	}

	root->isRed = false; // 根节点始终为黑
}

template <typename T>
Node<T> *RBTree<T>::findMin(Node<T> *node) const {
	while (node != nil && node->left != nil) {
		node = node->left;
	}
	return node;
}

template <typename T>
Node<T> *RBTree<T>::findMax(Node<T> *node) const {
	while (node != nil && node->right != nil) {
		node = node->right;
	}
	return node;
}

template <typename T>
Node<T> *RBTree<T>::successor(Node<T> *node) const {
	if (node->right != nil) {
		return minimum(node->right);
	}
	Node<T> *y = node->parent;
	while (y != nil && node == y->right) {
		node = y;
		y = y->parent;
	}
	return y;
}

template <typename T>
Node<T> *RBTree<T>::predecessor(Node<T> *node) const {
	if (node->left != nil) {
		return maximum(node->left);
	}
	Node<T> *y = node->parent;
	while (y != nil && node == y->left) {
		node = y;
		y = y->parent;
	}
	return y;
}

template <typename T>
void RBTree<T>::transplant(Node<T> *u, Node<T> *v) {
	if (u->parent == nil)
		root = v;
	else if (u == u->parent->left)
		u->parent->left = v;
	else
		u->parent->right = v;
	v->parent = u->parent;
}

template <typename T>
void RBTree<T>::erase(const T &data) {
	Node<T> *node = find(data);
	if (node == nullptr)
		return;

	Node<T> *x, *y = node;
	bool yOriginalColor = y->isRed;

	if (node->left == nil) { // 左子树为空,用右子树替代
		x = node->right;
		transplant(node, node->right);
	} else if (node->right == nil) { // 右子树为空,用左子树替代
		x = node->left;
		transplant(node, node->left);
	} else { // 左右子树均不为空,找到后继节点
		y = findMin(node->right);
		yOriginalColor = y->isRed;
		x = y->right;
		if (y->parent == node) { // 后继节点是待删除节点的右子节点,直接用右子节点替代
			if (x != nil)
				x->parent = y;
		} else { // 用后继节点的右子节点替代后继节点
			transplant(y, y->right);
			y->right = node->right;
			y->right->parent = y;
		}
		transplant(node, y); // 用后继节点替代待删除节点
		y->left = node->left;
		y->left->parent = y;
		y->isRed = node->isRed;
	}
	if (!yOriginalColor) { // 删除节点为黑色,修复红黑树性质
		eraseFixUp(x);
	}
	delete node;
}

template <typename T>
void RBTree<T>::eraseFixUp(Node<T> *node) {
	while (node != root && !node->isRed) { // 当前节点不是根节点且不是红色
		if (node == node->parent->left) {  // 当前节点是左子节点
			Node<T> *bro = node->parent->right;
			if (bro->isRed) { // 兄弟节点是红色, 父节点变红,兄弟节点变黑,左旋父节点,继续修复
				bro->isRed = false;
				node->parent->isRed = true;
				rotateLeft(node->parent);
				bro = node->parent->right;
			}
			if (!bro->left->isRed && !bro->right->isRed) { // 兄弟节点的两个子节点都是黑色,兄弟节点变红,上移继续修复
				bro->isRed = true;
				node = node->parent;
			} else { // 兄弟节点至少有一个红色子节点
				// 兄弟节点的右子节点是黑色,左子节点是红色,RL型,兄弟节点变红,其左子节点变黑,右旋兄弟节点变成RR型
				if (!bro->right->isRed) {
					bro->left->isRed = false;
					bro->isRed = true;
					rotateRight(bro);
					bro = node->parent->right;
				}
				// RR型,兄弟节点的右子节点是红色,父节点颜色给兄弟节点,父节点变黑,右子变黑,左旋父节点
				bro->isRed = node->parent->isRed;
				node->parent->isRed = false;
				bro->right->isRed = false;
				rotateLeft(node->parent);
				node = root;
			}
		} else {
			Node<T> *bro = node->parent->left;
			if (bro->isRed) {
				bro->isRed = false;
				node->parent->isRed = true;
				rotateRight(node->parent);
				bro = node->parent->left;
			}
			if (!bro->right->isRed && !bro->left->isRed) {
				bro->isRed = true;
				node = node->parent;
			} else {
				// LR型,兄弟节点的左子节点是黑色,兄弟节点变红,其右子节点变黑,左旋兄弟节点变成RR型
				if (!bro->left->isRed) {
					bro->right->isRed = false;
					bro->isRed = true;
					rotateLeft(bro);
					bro = node->parent->left;
				}
				// RR型,兄弟节点的左子节点是红色,父节点颜色给兄弟节点,父节点变黑,左子变黑,右旋父节点
				bro->isRed = node->parent->isRed;
				node->parent->isRed = false;
				bro->left->isRed = false;
				rotateRight(node->parent);
				node = root;
			}
		}
	}
	node->isRed = false;
}

#include <iostream>

int main() {
	RBTree<int> tree;
	tree.insert(10);
	tree.insert(5);
	tree.insert(15);
	tree.insert(3);
	tree.insert(7);
	tree.insert(13);
	tree.insert(17);


	std::cout << "Searching for 7: " << (tree.find(7) != nullptr ? "Found" : "Not Found") << std::endl;
	std::cout << "Searching for 8: " << (tree.find(8) != nullptr ? "Found" : "Not Found") << std::endl;

	tree.erase(7);
	
	std::cout << "Searching for 7: " << (tree.find(7) != nullptr ? "Found" : "Not Found") << std::endl;

	return 0;
}

结果输出:

Searching for 7: Found
Searching for 8: Not Found
Searching for 7: Not Found

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions