我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C++高级数据结构之二叉查找树

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C++高级数据结构之二叉查找树

前言:

  • 文章目录
  • 基础概念
  • 基本实现
  • 有序性相关的方法
  • 与删除相关的方法
  • 性能分析
  • 完整代码和测试

高级数据结构(Ⅳ)二叉查找树

基础概念

此数据结构由结点组成,结点包含的链接可以为空(null)或者指向其他结点。在二叉树中,每个结点只能有一个父结点(只有一个例外,也就是根结点,它没有父结点),而且每个结点都只有左右两个链接,分别指向自己的左子结点和右子结点。每个结点的两个链接都指向了一棵独立的子二叉树或空链接。在二叉查找树中,每个结点还包含了一个键和一个值,键之间也有顺序之分以支持高校的查找。

定义:一棵二叉查找树(BST)是一棵二叉树,
其中每个结点都含有一个Comparable的键(以及相关联的值)
且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键。

以int类型为键,string类型为值的二叉查找树的API如下:

class BST<Key extends Comparable<Key>, Value>
-------------------------------------------------------------------------------------
Node root; 根结点
int size(Node x); 返回以结点x为根节点的子树大小
Value get(Key key) 返回键key对应的值
void put(Key key, Value val) 插入键值对{key : val}
Key min() 返回最小键
Key max() 返回最大键
Key floor(Key key) 向下取整(返回小于等于key的最大值)
Key ceiling(Key key) 向上取整(返回大于等于key的最小值)
Key select(int k) 返回排名为k的键
int rank(Key key) 返回键key的排名
Iterable<Key> keys(Key lo, Key hi) 返回查找(返回指定范围内的所有键值)
void deleteMin() 删除最小键
void deleteMax() 删除最大键
void delete (Key key) 删除键key

基本实现

数据表示

我们嵌套定义一个私有类来表示二叉查找树上的一个结点。每个结点都含有一个键、一个值、一条左链接、一条右链接和一个结点计数器。左链接指向一棵由小于该结点的所有键组成的二叉查找树,右链接指向一棵由大于该结点的所有键组成的二叉查找树。变量N给出了以该结点为根的子树的结点总数。

实现:

class BST<Key extends Comparable<Key>, Value> {
private Node root; //二叉查找树的根节点
private class Node{
private Key key; //键
private Value val; //值
private Node left, right; //指向子树的链接
private int N; //以该结点为根的子树中的结点总数
public Node(Key key, Value val, int N) {
this.key = key;
this.val = val;
this.N = N;
}
}
public int size() {
return size(root);
}
private int size(Node x) {
if (x == null) {
return 0;
} else {
return x.N;
}
}
}

一棵二叉查找树代表了一组键(及其相应的值)的集合,而同一个集合可以用多棵不同的二叉查找树表示。如果我们将一棵二叉查找树的所有键投影到一条直线上,保证一个结点的左子树中的键出现在它的左边,右子树中的键出现在它的右边,那么我们一定可以得到一条有序的键列。

查找

一般来说,在符号表中查找一个键可能得到两种结果。如果含有该键的结点存在于表中,我们的查找就命中了,然后返回相应的值。否则查找未命中(并返回null)。根据数据表示的递归结构我们马上就能得到,在二叉查找树中查找一个键的递归算法:如果树是空的,则查找未命中;如果被查找的键和根结点的键相等,查找命中,否则我们就(递归地)在适当的子树中继续查找。如果被查找的键较小就选择左子树,较大则选择右子树。


public Value get(Key key) {
return get(root, key);
}
private Value get(Node x, Key key) {
//在以x为根节点的子树中查找并返回key所对应的值
//如果找不到则返回null
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return get(x.left, key);
} else if (cmp > 0) {
return get(x.right, key);
} else {
return x.val;
}
}

插入

二叉查找树插入的实现难度和查找差不多。如果树是空的,就返回一个含有该键值对的新结点;如果被查找的键小于根结点的键,继续在左子树中搜索插入该键,否则在右子树中插入该键。


public void put(Key key, Value val) {
//查找key,找到则更新它的值,否则为它创建一个新的结点
root = put(root, key, val);
}
private Node put(Node x, Key key, Value val) {
//如果key存在于以x为根结点的子树中则更新它的值;
//否则将以key和val为键值对的新结点插入到该子树中
if (x == null) {
return new Node(key, val, 1);
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x.left = put(x.left, key, val);
} else if (cmp > 0) {
x.right = put(x.right, key, val);
} else {
x.val = val;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}

有序性相关的方法

二叉查找树得以广泛应用的一个重要原因就是它能够保持键的有序性,因此它可以作为实现有序符号表API中的众多方法的基础。这使得符号表的用例不仅能够通过键还能通过键的相对顺序来访问键值对。

最小键和最大键

如果根节点的左链接为空,那么一棵二叉查找树中最小的键就是根结点;如果左链接非空,那么树中的最小键就是左子树中的最小键,显示可以由递归操作实现。

找出最大键的方法也是类似的,只不过是变为查找右子树而已。

最小键


public Key min() {
if (root == null) {
return null;
}
return min(root).key;
}
private Node min(Node x) {
if (x.left == null) {
return x;
}
return min(x.left);
}

最大键


public Key max() {
if (root == null) {
return null;
}
return max(root).key;
}
private Node max(Node x) {
if (x.right == null) {
return x;
}
return max(x.right);
}

向上取整和向下取整

如果给定的键key小于二叉查找树的根结点的键,那么小于等于key的最大键floor(key)一定在根结点的左子树中;如果给定的键key大于二叉查找树的根结点,那么只有当根结点右子树中存在小于等于key的结点时,小于等于key的最大键才会出现在右子树中,否则根结点就是小于等于key的最大键。这段描述说明了floor()方法的递归实现,同时也递推地证明了它能够计算出预期的结果。将“左”变为“右”(同时将小于变为大于)就能够得到ceiling()的算法。

向下取整


public Key floor(Key key) {
Node x = floor(root, key);
if (x == null) {
return null;
}
return x.key;
}
private Node floor(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp == 0) {
return x;
}
if (cmp < 0) {
return floor(x.left, key);
}
Node t = floor(x.right, key);
if (t != null) {
return t;
} else {
return x;
}
}

向上取整


public Key ceiling(Key key) {
Node x = ceiling(root, key);
if (x == null) {
return null;
}
return x.key;
}
private Node ceiling(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp == 0) {
return x;
}
if (cmp > 0) {
return ceiling(x.right, key);
}
Node t = ceiling(x.left, key);
if (t != null) {
return t;
} else {
return x;
}
}

选择操作


public Key select(int k) {
if (root == null) {
return null;
}
return select(root, k).key;
}
private Node select(Node x, int k) {
//返回排名为k结点
if (x == null) {
return null;
}
//注:书中此处为int t = size(x.left);
//个人觉得此处应该加上当前结点,即+1(若有建议欢迎指正)
int t = size(x.left) + 1;
if (t > k) {
return select(x.left, k);
} else if (t < k) {
return select(x.right, k - t - 1);
} else {
return x;
}
}

二叉查找树的选择操作和基于切分的数组选择操作类似。我们在二叉查找树中的每个结点中维护的子树结点计数器变量​​N​​就是用来支持此操作的。

排名


public int rank(Key key) {
if (root == null) {
return 0;
}
return rank(root, key);
}
private int rank(Node x, Key key) {
//返回以x为根结点的子树中小于x.key的键的数量
if (x == null) {
return 0;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return rank(x.left, key);
} else if (cmp > 0) {
return 1 + size(x.left) + rank(x.right, key);
} else {
//若返回给定键的排名,我认为这里要+1,不然可以在上面调用后的return语句后+1
//书中此处为return size(x.left)(若有建议欢迎指正);
return size(x.left) + 1;
}
}

rank()方法是select()方法的逆方法,它会返回给定键的排名。它的实现和select()类似:如果给定的键和根结点的键相等,我们返回左子树中的结点总数t;如果给定的键小于根结点,我们会返回该键在左子树中的排名(递归计算);如果给定的键大于根结点,我们会返回t+1(根结点)加上它在右子树中的排名(递归计算)。

范围查找


public Iterable<Key> keys() {
return keys(min(), max());
}

public Iterable<Key> keys(Key lo, Key hi) {
Queue<Key> queue = new LinkedList<Key>();
keys(root, queue, lo, hi);
return queue;
}
private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
if (x == null) {
return;
}
int cmplo = lo.compareTo(x.key);
int cmphi = hi.compareTo(x.key);
if (cmplo < 0) {
keys(x.left, queue, lo, hi);
}
if (cmplo <= 0 && cmphi >= 0) {
queue.offer(x.key);
}
if (cmphi > 0) {
keys(x.right, queue, lo, hi);
}
}

要实现能够返回给定范围键的keys()方法,我们首先需要一个遍历二叉查找树的基本方法,为中序遍历。要说明这个方法,我们先看看如何能够将二叉查找树中的所有键按照顺序打印出来。要做到这一点,我们应该先打印出根结点的 左子树中的所有键(根据二叉查找树的定义它们应该都小于根结点的键),然后打印出根结点的键,最后打印出根结点的右子树中的所有键(根据二叉查找树的定义它们应该都大于根结点的键)。

与删除相关的方法

删除最小键

二叉查找树中最难实现的方法就是delete()方法,即从符号表中删除一个键值对。在此之前,我们先考虑deleteMin()方法(删除最小键所对应的键值对)。对于deleteMin(),我们要不断深入根节点的左子树直至遇见一个空链接,然后将指向该结点的右子树(只需要在递归调用中返回它的右链接即可)。此时它会被垃圾收集器清理掉。我们给出的标准递归代码在删除结点后会正确地设置它的父结点的链接并更新它到根结点的路径上的所有结点的计数器的值。


public void deleteMin() {
if (root == null) {
return;
}
deleteMin(root);
}
private Node deleteMin(Node x) {
if (x.left == null) {
return x.right;
}
x.left = deleteMin(x.left);
x.N = size(x.left) + size(x.right) + 1;
return x;
}

删除最大键

deletemax()方法的实现和deletemin()完全类似,相应地,只需删除右子树最右端结点,然后返回其最右端结点的左子树即可。


public void deleteMax() {
if (root == null) {
return;
}
deleteMax(root);
}
private Node deleteMax(Node x) {
if (x.right == null) {
return x.left;
}
x.right = deleteMax(x.right);
x.N = size(x.left) + size(x.right) + 1;
return x;
}

删除操作

  • 将指向即将被删除的结点的链接保存为t;
  • 将x指向它的后继结点min(t.right);
  • 将x的右链接(原本指向一棵所有结点都大于x.key的二叉查找树)指向deleteMin(t.right),也就是在删除后所有结点仍然都大于x.key的子二叉查找树;
  • 将x的左链接(本为空)设为t.left(其下所有的键都小于被删除的结点和它的后继结点)。

实现:


public void delete (Key key) {
root = delete(root, key);
}

private Node delete(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x.left = delete(x.left, key);
} else if(cmp > 0) {
x.right = delete(x.right, key);
} else {
if (x.right == null) {
return x.left;
}
if (x.left == null) {
return x.right;
}
Node t = x;
x = min(t.right);
x.right = deleteMin(t.right);
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}

性能分析

我见过二叉查找树,但它的实现没有使用递归。这两种方式各有哪些优缺点?
答:一般来说,递归的实现更容易验证其正确性,而非递归的实现效率更高。

完整代码和测试

完整代码

class BST<Key extends Comparable<Key>, Value> {
private Node root; //二叉查找树的根节点
private class Node{
private Key key; //键
private Value val; //值
private Node left, right; //指向子树的链接
private int N; //以该结点为根的子树中的结点总数

public Node(Key key, Value val, int N) {
this.key = key;
this.val = val;
this.N = N;
}
}

public int size() {
return size(root);
}

private int size(Node x) {
if (x == null) {
return 0;
} else {
return x.N;
}
}


public Value get(Key key) {
return get(root, key);
}

private Value get(Node x, Key key) {
//在以x为根节点的子树中查找并返回key所对应的值
//如果找不到则返回null
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return get(x.left, key);
} else if (cmp > 0) {
return get(x.right, key);
} else {
return x.val;
}
}


public void put(Key key, Value val) {
//查找key,找到则更新它的值,否则为它创建一个新的结点
root = put(root, key, val);
}

private Node put(Node x, Key key, Value val) {
//如果key存在于以x为根结点的子树中则更新它的值;
//否则将以key和val为键值对的新结点插入到该子树中
if (x == null) {
return new Node(key, val, 1);
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x.left = put(x.left, key, val);
} else if (cmp > 0) {
x.right = put(x.right, key, val);
} else {
x.val = val;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}


public Key min() {
if (root == null) {
return null;
}
return min(root).key;
}

private Node min(Node x) {
if (x.left == null) {
return x;
}
return min(x.left);
}


public Key max() {
if (root == null) {
return null;
}
return max(root).key;
}

private Node max(Node x) {
if (x.right == null) {
return x;
}
return max(x.right);
}


public Key floor(Key key) {
Node x = floor(root, key);
if (x == null) {
return null;
}
return x.key;
}

private Node floor(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp == 0) {
return x;
}
if (cmp < 0) {
return floor(x.left, key);
}
Node t = floor(x.right, key);
if (t != null) {
return t;
} else {
return x;
}
}


public Key ceiling(Key key) {
Node x = ceiling(root, key);
if (x == null) {
return null;
}
return x.key;
}
private Node ceiling(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp == 0) {
return x;
}
if (cmp > 0) {
return ceiling(x.right, key);
}
Node t = ceiling(x.left, key);
if (t != null) {
return t;
} else {
return x;
}
}

public Key select(int k) {
if (root == null) {
return null;
}
return select(root, k).key;
}
private Node select(Node x, int k) {
//返回排名为k结点
if (x == null) {
return null;
}
int t = size(x.left) + 1;
if (t > k) {
return select(x.left, k);
} else if (t < k) {
return select(x.right, k - t - 1);
} else {
return x;
}
}

public int rank(Key key) {
if (root == null) {
return 0;
}
return rank(root, key);
}

private int rank(Node x, Key key) {
//返回以x为根结点的子树中小于x.key的键的数量
if (x == null) {
return 0;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
return rank(x.left, key);
} else if (cmp > 0) {
return 1 + size(x.left) + rank(x.right, key);
} else {
return size(x.left) + 1;
}
}


public Iterable<Key> keys() {
return keys(min(), max());
}

public Iterable<Key> keys(Key lo, Key hi) {
Queue<Key> queue = new LinkedList<Key>();
keys(root, queue, lo, hi);
return queue;
}

private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
if (x == null) {
return;
}
int cmplo = lo.compareTo(x.key);
int cmphi = hi.compareTo(x.key);
if (cmplo < 0) {
keys(x.left, queue, lo, hi);
}
if (cmplo <= 0 && cmphi >= 0) {
queue.offer(x.key);
}
if (cmphi > 0) {
keys(x.right, queue, lo, hi);
}
}


public void deleteMin() {
if (root == null) {
return;
}
deleteMin(root);
}

private Node deleteMin(Node x) {
if (x.left == null) {
return x.right;
}
x.left = deleteMin(x.left);
x.N = size(x.left) + size(x.right) + 1;
return x;
}

public void deleteMax() {
if (root == null) {
return;
}
deleteMax(root);
}
private Node deleteMax(Node x) {
if (x.right == null) {
return x.left;
}
x.right = deleteMax(x.right);
x.N = size(x.left) + size(x.right) + 1;
return x;
}


public void delete (Key key) {
root = delete(root, key);
}

private Node delete(Node x, Key key) {
if (x == null) {
return null;
}
int cmp = key.compareTo(x.key);
if (cmp < 0) {
x.left = delete(x.left, key);
} else if(cmp > 0) {
x.right = delete(x.right, key);
} else {
if (x.right == null) {
return x.left;
}
if (x.left == null) {
return x.right;
}
Node t = x;
x = min(t.right);
x.right = deleteMin(t.right);
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}
}

测试

int[] keys = {6, 5, 1, 3, 2, 4, 9, 7, 8, 10};
String[] values = {"F", "E", "A", "C", "B", "D", "I", "G", "H", "J"};

public class BSTTest {
public static void main(String[] args) {
int[] keys = {6, 5, 1, 3, 2, 4, 9, 7, 8, 10};
String[] values = {"F", "E", "A", "C", "B", "D", "I", "G", "H", "J"};

BST<Integer, String> bst = new BST<Integer, String>();
//构建树
for (int i = 0; i < keys.length; i++) {
bst.put(keys[i], values[i]);
}

System.out.println("创建树的键为:");
LinkedList<Integer> queue = (LinkedList<Integer>) bst.keys();
while (!queue.isEmpty()) {
System.out.print(queue.poll() + " ");
}

System.out.println("\n创建树的大小为:" + bst.size());
System.out.println("键3所对应的值为:" + bst.get(3));
System.out.println("最小键为:" + bst.min());
System.out.println("最大键为: " + bst.max());
System.out.println("小于等于11的最大键是:" + bst.floor(11));
System.out.println("大于等于0的最小键是: " + bst.ceiling(0));
System.out.println("排名为5的键是:" + bst.select(5));
System.out.println("键7的排名是:" + bst.rank(7));

System.out.println("\n键值在3-8之间的键有:");
LinkedList<Integer> queue1 = (LinkedList<Integer>) bst.keys(3, 8);
while (!queue1.isEmpty()) {
System.out.print(queue1.poll() + " ");
}
System.out.println("\n删除最小键和最大键后的键值为:");
bst.deleteMin();
bst.deleteMax();
LinkedList<Integer> queue2 = (LinkedList<Integer>) bst.keys();
while (!queue2.isEmpty()) {
System.out.print(queue2.poll() + " ");
}
System.out.println("\n删除键4后的键值为: ");
bst.delete(4);
LinkedList<Integer> queue3 = (LinkedList<Integer>) bst.keys();
while (!queue3.isEmpty()) {
System.out.print(queue3.poll() + " ");
}
}
}

创建树的键为:
1 2 3 4 5 6 7 8 9 10
创建树的大小为:10
键3所对应的值为:C
最小键为:1
最大键为: 10
小于等于11的最大键是:10
大于等于0的最小键是: 1
排名为5的键是:5
键7的排名是:7
键值在3-8之间的键有:
3 4 5 6 7 8
删除最小键和最大键后的键值为:
2 3 4 5 6 7 8 9
删除键4后的键值为:
2 3 5 6 7 8 9
Process finished with exit code 0

到此这篇关于C++高级数据结构之二叉查找树的文章就介绍到这了,更多相关C++二叉查找树内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C++高级数据结构之二叉查找树

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C++高级数据结构之二叉查找树怎么实现

本文小编为大家详细介绍“C++高级数据结构之二叉查找树怎么实现”,内容详细,步骤清晰,细节处理妥当,希望这篇“C++高级数据结构之二叉查找树怎么实现”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。高级数据结构(Ⅳ)
2023-06-30

数据结构TypeScript之二叉查找树实现详解

这篇文章主要为大家介绍了数据结构TypeScript之二叉查找树实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-30

数据结构之链式二叉树详解

所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。本文通过代码示例详细介绍了C语言中的链式二叉树,需要的朋友可以参考一下
2023-05-16

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录