二叉搜索树的局限性

-------------------siwuxie095

二叉搜索树的局限性

二叉搜索树在时间性能上是具有局限性的

同样的数据,可以对应不同的二叉搜索树,如下:

二叉搜索树可能退化成链表,相应的,二叉搜索树的查找操作是和这棵树

的高度相关的,而此时这颗树的高度就是这颗树的节点数
n,同时二叉搜

索树相应的算法全部退化成 O(n) 级别

显然,说二叉搜索树的查找、插入、删除
这三个操作都是
O(lgn) 级别的,

只是一个大概的估算,具体要和二叉搜索树的形状相关

二叉搜索树并不能像堆那样,保证所有的操作都一定是 O(lgn) 级别的,

为了形象展示二叉搜索树的局限性,实际做一个测试

程序:二叉搜索树和顺序查找表在数组有序情况下的对比

FileOps.h:


#ifndef FILEOPS_H

#define FILEOPS_H

#include <string>

#include <iostream>

#include <fstream>

#include <vector>

using namespace std;

namespace FileOps

{

int firstCharacterIndex(const string &s, int start)

{

for (int i = start; i < s.length(); i++)

{

if (isalpha(s[i]))

{

return i;

}

}

return s.length();

}

//把大写字符串转换为小写字符串

string lowerS(const string &s)

{

string ret = "";

for (int i = 0; i < s.length(); i++)

{

ret += tolower(s[i]);

}

return ret;

}

//将文件读入words数组中

bool readFile(const string& filename, vector<string> &words)

{

string line;

string contents = "";

ifstream file(filename);

if (file.is_open())

{

while (getline(file, line))

{

contents += (line + "\n");

}

file.close();

}

else

{

cout << "Can not open " << filename << " !!!" << endl;

return false;

}

int start = firstCharacterIndex(contents, 0);

for (int i = start + 1; i <= contents.length();)

{

if (i == contents.length() || !isalpha(contents[i]))

{

words.push_back(lowerS(contents.substr(start, i - start)));

start = firstCharacterIndex(contents, i);

i = start + 1;

}

else

{

i++;

}

}

return true;

}

}

#endif

BST.h:


#ifndef BST_H

#define BST_H

#include
"stdlib.h"

#include <queue>

//二叉搜索树

template <typename Key, typename Value>

class BST

{

private:

struct Node

{

Key key;

Value value;

Node *left;

Node *right;

Node(Key key, Value value)

{

this->key = key;

this->value = value;

this->left = this->right = NULL;

}

Node(Node *node)

{

this->key = node->key;

this->value = node->value;

this->left = node->left;

this->right = node->right;

}

};

Node *root; //根节点

int count;

public:

BST()

{

root = NULL;

count = 0;

}

~BST()

{

destroy(root);

}

int size()

{

return count;

}

bool isEmpty()

{

return count == 0;

}

//向整棵二叉树树中插入新元素转换成向一个子树中插入新元素

//直到子树是空的时候,新建一个节点,这个新建的节点就是一

//棵新的子树,只不过它只有一个节点,将它直接返回回去

//

//这样,通过递归的方式向二叉搜索树中插入了一个新的元素

void insert(Key key, Value value)

{

root = insert(root, key, value);

}

bool contain(Key key)

{

return contain(root, key);

}

//search()函数常见的返回形式:

//(1)Node*,缺点:对外界来说,没有将数据结构Node进行隐藏

//(2)Value,缺点:如果查找不到的话,不知道该返回什么数值

//(3)Value*,优点:作为一个指针可以存一个空元素

Value *search(Key key)

{

return search(root, key);

}

// 前序遍历

void preOrder()

{

preOrder(root);

}

// 中序遍历:会将二叉搜索树的key从小到大进行排序

void inOrder()

{

inOrder(root);

}

// 后序遍历

void postOrder()

{

postOrder(root);

}

// 层序遍历

void levelOrder()

{

//需要引入队列:先进先出

queue<Node*> q;

q.push(root);

while (!q.empty())

{

Node *node = q.front();

q.pop();

cout << node->key << endl;

//如果node的左孩子不为空

if (node->left)

{

q.push(node->left);

}

//如果node的右孩子不为空

if (node->right)

{

q.push(node->right);

}

}

}

// 寻找最小的键值

Key minimum()

{

assert(count != 0);

Node *minNode = minimum(root);

return minNode->key;

}

// 寻找最大的键值

Key maximum()

{

assert(count != 0);

Node *maxNode = maximum(root);

return maxNode->key;

}

// 从二叉树中删除最小值所在节点

void removeMin()

{

//根节点不为空,才能做事情

if (root)

{

root = removeMin(root);

}

}

// 从二叉树中删除最大值所在节点

void removeMax()

{

if (root)

{

root = removeMax(root);

}

}

// 从二叉树中删除键值为key的节点

void remove(Key key)

{

root = remove(root, key);

}

private:

// 向以node为根的二叉搜索树中,插入节点(key, value)

// 返回插入新节点后的二叉搜索树的根

Node *insert(Node *node, Key key, Value value)

{

//递归到底的情况:如果一个节点都没有,

//创建一个新节点作为子树的根

if (node == NULL)

{

count++;

return new Node(key, value);

}

//如果新插入节点的key等于当前节点的key,做更新操作即可

if (key == node->key)

{

node->value = value;

}

else if (key < node->key)

{

node->left = insert(node->left, key, value);

}

else

{

// key > node->key

node->right = insert(node->right, key, value);

}

return node;

}

// 查看以node为根的二叉搜索树中是否包含键值为key的节点

bool contain(Node *node, Key key)

{

//如果当前访问的节点已经为空,

//即不包含,直接返回false即可

if (node == NULL)

{

return false;

}

if (key == node->key)

{

return true;

}

else if (key < node->key)

{

return contain(node->left, key);

}

else

{

// key > node->key

return contain(node->right, key);

}

}

// 在以node为根的二叉搜索树中查找key所对应的value

Value *search(Node *node, Key key)

{

if (node == NULL)

{

return NULL;

}

if (key == node->key)

{

return &(node->value);

}

else if (key < node->key)

{

return search(node->left, key);

}

else

{

// key > node->key

return search(node->right, key);

}

}

// 对以node为根的二叉搜索树进行前序遍历

void preOrder(Node *node)

{

if (node != NULL)

{

cout << node->key << endl;

preOrder(node->left);

preOrder(node->right);

}

}

// 对以node为根的二叉搜索树进行中序遍历

void inOrder(Node *node)

{

if (node != NULL)

{

inOrder(node->left);

cout << node->key << endl;

inOrder(node->right);

}

}

// 对以node为根的二叉搜索树进行后序遍历

void postOrder(Node *node)

{

if (node != NULL)

{

postOrder(node->left);

postOrder(node->right);

cout << node->key << endl;

}

}

void destroy(Node *node)

{

//使用后序操作的方式来释放整棵树

if (node != NULL)

{

destroy(node->left);

destroy(node->right);

delete node;

count--;

}

}

// 在以node为根的二叉搜索树中,返回最小键值的节点

Node *minimum(Node *node)

{

if (node->left == NULL)

{

return node;

}

return minimum(node->left);

}

// 在以node为根的二叉搜索树中,返回最大键值的节点

Node *maximum(Node *node)

{

if (node->right == NULL)

{

return node;

}

return maximum(node->right);

}

// 删除掉以node为根的二叉搜索树中的最小节点

// 返回删除节点后新的二叉搜索树的根

Node *removeMin(Node *node)

{

//如果当前节点的左孩子为空,则当前节点为最小节点

//显然,最小值所在的节点只可能有右孩子

if (node->left == NULL)

{

Node *rightNode = node->right;

delete node;

count--;

return rightNode;

}

node->left = removeMin(node->left);

return node;

}

// 删除掉以node为根的二叉搜索树中的最大节点

// 返回删除节点后新的二叉搜索树的根

Node* removeMax(Node* node)

{

//如果当前节点的右孩子为空,则当前节点为最大节点

//显然,最大值所在的节点只可能有左孩子

if (node->right == NULL)

{

Node *leftNode = node->left;

delete node;

count--;

return leftNode;

}

node->right = removeMax(node->right);

return node;

}

// 删除掉以node为根的二叉搜索树中键值为key的节点

// 返回删除节点后新的二叉搜索树的根

Node* remove(Node* node, Key key)

{

if (node == NULL)

{

return NULL;

}

if (key < node->key)

{

node->left = remove(node->left, key);

return node;

}

else if (key > node->key)

{

node->right = remove(node->right, key);

return node;

}

else

{ // key == node->key

//如果node只有右孩子

if (node->left == NULL)

{

Node *rightNode = node->right;

delete node;

count--;

return rightNode;

}

//如果node只有左孩子

if (node->right == NULL)

{

Node *leftNode = node->left;

delete node;

count--;

return leftNode;

}

// node->left != NULL && node->right != NULL

//即node的左右孩子都不为空

Node *successor = new Node(minimum(node->right));

count++;

successor->right = removeMin(node->right);

successor->left = node->left;

delete node;

count--;

return successor;

}

}

};

//前中后序遍历属于深度优先遍历

//而层序遍历则属于广度优先遍历

//

//这四种遍历方式相对都非常高效,时间复杂度是O(n)

//

//

//

//二叉搜索树中,最复杂的一个操作就是删除节点

//

//其实删除一个节点很容易,关键是将这个节点删除之后,如何来处理

//与这个节点相关联的部分,使得整棵树依然保持二叉搜索树的性质

//

//

//如果要删除的节点只有一个孩子,那么这个问题很简单,和删除最大

//(小)值所在节点的方法一样,最难的是删除左右都有孩子的节点

//

//处理这种情况的一个非常经典的算法就是Hibbard Deletion,这个算

//法在 1962 年被一个叫做 Hibbard 的计算机科学家提出,具体如下:

//

//假如这个被删除的节点是 d,d 既有左孩子,又有右孩子,其实要做

//的事情就是找一个节点来代替 d,这个节点既不应该是 d 的左孩子,

//也不应该是 d 的右孩子,Hibbard 提出这个节点应该是 d 的右子树

//中的最小值

//

//删除左右都有孩子的节点 d,用 s 来代替,即 s 是 d 的后继

//(d 即 deletion,s 即 successor)

//

//s=min(d->right)

//s->right=delMin(d->right)

//s->left=d->left

//

//删除 d,s 是新的子树的根

//

//

//

//其实代替的节点也可以是 d 的左子树中的最大值,如下:

//

//删除左右都有孩子的节点 d,用 p 来代替,p 是 d 的前驱

//(d 即 deletion,p 即 predecessor)

//

//p=max(d-left)

//p->left=delMax(d->left)

//p->right=d->right

//

//删除 d,p 是新的子树的根

//

//

//删除二叉搜索树中的任意一个节点
时间复杂度 O(lgn)

#endif

SequenceST.h:


#ifndef SEQUENCEST_H

#define SEQUENCEST_H

#include <iostream>

#include <cassert>

using namespace std;

//顺序查找表:采用链表的数据结构实现

template<typename Key, typename Value>

class SequenceST

{

private:

struct Node

{

Key key;

Value value;

Node *next;

Node(Key key, Value value)

{

this->key = key;

this->value = value;

this->next = NULL;

}

};

Node* head;

int count;

public:

SequenceST()

{

head = NULL;

count = 0;

}

~SequenceST()

{

while (head != NULL)

{

Node *node = head;

head = head->next;

delete node;

count--;

}

assert(head == NULL && count == 0);

}

int size()

{

return count;

}

bool isEmpty()

{

return count == 0;

}

void insert(Key key, Value value)

{

Node *node = head;

while (node != NULL)

{

if (key == node->key)

{

node->value = value;

return;

}

node = node->next;

}

Node *newNode = new Node(key, value);

newNode->next = head;

head = newNode;

count++;

}

bool contain(Key key)

{

Node *node = head;

while (node != NULL)

{

if (key == node->key)

{

return true;

}

node = node->next;

}

return false;

}

Value* search(Key key)

{

Node *node = head;

while (node != NULL)

{

if (key == node->key)

{

return &(node->value);

}

node = node->next;

}

return NULL;

}

void remove(Key key)

{

if (key == head->key)

{

Node* delNode = head;

head = head->next;

delete delNode;

count--;

return;

}

Node *node = head;

while (node->next != NULL && node->next->key != key)

{

node = node->next;

}

if (node->next != NULL)

{

Node* delNode = node->next;

node->next = delNode->next;

delete delNode;

count--;

return;

}

}

};

#endif

main.cpp:


#include
"FileOps.h"

#include
"BST.h"

#include
"SequenceST.h"

#include <iostream>

#include <vector>

#include <string>

#include <ctime>

using namespace std;

int main()

{

//把英文版共产主义宣言作为测试用例

string filename = "communist.txt";

vector<string> words;

if (FileOps::readFile(filename, words))

{

cout << "There are totally " << words.size() << " words in " << filename << endl;

cout << endl;

// test BST

time_t startTime = clock();

BST<string, int> *bst = new BST<string, int>();

for (vector<string>::iterator iter = words.begin(); iter != words.end(); iter++)

{

int *res = (*bst).search(*iter);

if (res == NULL)

{

(*bst).insert(*iter, 1);

}

else

{

(*res)++;

}

}

//查找操作:单词 unite 的词频

cout << "‘unite‘ : " << *(*bst).search("unite") << endl;

time_t endTime = clock();

cout << "BST , time: " << double(endTime - startTime) / CLOCKS_PER_SEC

<< " s." << endl;

cout << endl;

delete bst;

// test SST

startTime = clock();

SequenceST<string, int> *sst = new SequenceST<string, int>();

for (vector<string>::iterator iter = words.begin(); iter != words.end(); iter++)

{

int *res = (*sst).search(*iter);

if (res == NULL)

{

(*sst).insert(*iter, 1);

}

else

{

(*res)++;

}

}

cout << "‘unite‘ : " << *(*sst).search("unite") << endl;

endTime = clock();

cout << "SST , time: " << double(endTime - startTime) / CLOCKS_PER_SEC

<< " s." << endl;

cout << endl;

delete sst;

// test BST2

startTime = clock();

BST<string, int> *bst2 = new BST<string, int>();

//在向二叉搜索树中插入节点之前,将words数组进行一次排序,

//使得之后,按照从小到大的顺序逐一将单词放到二叉搜索树中

sort(words.begin(), words.end());

for (vector<string>::iterator iter = words.begin(); iter != words.end(); iter++)

{

int *res = (*bst2).search(*iter);

if (res == NULL)

{

(*bst2).insert(*iter, 1);

}

else

{

(*res)++;

}

}

cout << "‘unite‘ : " << *(*bst2).search("unite") << endl;

endTime = clock();

cout << "BST2 , time: " << double(endTime - startTime) / CLOCKS_PER_SEC

<< " s." << endl;

cout << endl;

delete bst2;

}

system("pause");

return
0;

}

//为什么BST2比顺序查找表SST还要慢呢?原因如下:

//

//(1)顺序查找表SST用链表实现,它只处理一个指针,而BST2虽然退化

//成了链表,但在实现的每一步中,还是有左孩子这个概念的,所以要不

//停的判断左孩子为空这种情况,这就消耗了一定的性能

//

//(2)BST是采用递归的方式实现的,递归本身也会比在链表中采用迭代

//的方式实现,性能会慢一些,累积起来就有了时间差距

运行一览:

其实对于大多数情况,在正常使用中,BST
的性能非常好,出现非常极端的

退化情况的概率非常小

尽管如此,还是有机会出现这样的情况,可能有人就会想,可以像快速排序

那样,在初始化的时候,将数据打乱即可

当然,这是一个解决方案, 不过这个解决方案的缺点在于:需要一上来就拿

到所有的数据

可是在有些情况下,数据是慢慢的流入到系统的。如果在这个过程中,数据

是近乎有序的话,BST
的效率就令人担忧了

实际上,计算机科学家也为此想出了解决方案:可以改造二叉搜索树的实现,

使得二叉搜索树无法退化成链表,称这样的二叉搜索树为
平衡二叉搜索树,

简称
平衡二叉树(也称为
AVL 树)

「AVL 树中任何节点的两个子树的高度最大差别为一」

平衡二叉树的性质保证了整个二叉树的高度一定是
logN 级别的

平衡二叉树有诸多的实现,其中最为著名的一种实现,叫做
红黑树

红黑树非常创新的将节点分为了两类:红色节点

黑色节点,如下:

红黑树是每个节点都带有颜色属性的二叉搜索树,颜色为红色

黑色,

除了要满足一般二叉搜索树的要求,还要满足如下额外要求:

1)节点是红色

黑色

2)根是黑色

3)所有叶子都是黑色(叶子是
NIL
节点,即
空节点)

4)每个红色节点必须有两个黑色的子节点(从每个叶

子到根的所有路径上不能有两个连续的红色节点)

5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点

这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径

不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的

「注:nil 叶子 或 null 叶子」

另外,平衡二叉树还有其他的实现方式,如:2-3 树、伸展树

事实上,对于树这种数据结构而言,还有非常多的变种,如:平衡二叉树

和堆的结合
Treap

最后介绍一个叫做
Trie 树的数据结构,也叫做字典树 或 前缀树

对于二叉搜索树来说,一个经典的用途就是字典,但对于字典这样的实现

有一个问题:假如收录的单词非常多,几千万的数量级,那么即使是
lgn

这样的查找效率,其实也是非常慢的


trie 做到了使用一种数据结构,使得查找一个单词的定义,它的时间复

杂度是和单词本身的长度相关的,而和字典中到底有多少个单词无关

换句话说,不管字典里有多少个单词,要想查找
news
这个单词(4 个字母),

只需要找
4
个节点就够了

具体实现:在
trie
中,每个节点中会存一个字母,而一个单词则是从根节点

开始,依次向下到它的某一个孩子节点的这样的一个路径

不难想象,每一个节点除了存一个字母之外,相应的要存
26
个孩子的

指针

当然,还有一些细节要处理,这是因为对于某些单词来说,它们是另外

某些单词的子集,所以并非是所有单词都要遍历到叶子节点

【made by siwuxie095】

时间: 2024-10-15 13:52:41

二叉搜索树的局限性的相关文章

用JS实现二叉搜索树

二叉树的节点最多只能有两个子节点,一个左侧子节点,一个右侧子节点. 二叉搜索树(BST),是二叉树的一种,但只允许在左侧节点存储比父节点小的值,在右侧节点存储比父节点大或等于父节点的值. 1.创建BST 1.1创建BST类 首先申明BST类的基本结构 function BinarySearchTree() { var Node = function(key){ this.key = key; this.left = null; this.right = null; }; var root = n

538. Convert BST to Greater Tree 二叉搜索树转换为更大树

Given a Binary Search Tree (BST), convert it to a Greater Tree such that every key of the original BST is changed to the original key plus sum of all keys greater than the original key in BST. Example: Input: The root of a Binary Search Tree like thi

二叉搜索树

#include<stdio.h> #include<iostream> #include<math.h> #include<stdlib.h> using namespace std; struct TreeNode { TreeNode* p; TreeNode* l; TreeNode* r; int key; TreeNode() { p = 0; l = 0; r = 0; key = -1; } }; const int RANDMOD = 30

04-树4 是否同一棵二叉搜索树

给定一个插入序列就可以唯一确定一棵二叉搜索树.然而,一棵给定的二叉搜索树却可以由多种不同的插入序列得到.例如分别按照序列{2, 1, 3}和{2, 3, 1}插入初始为空的二叉搜索树,都得到一样的结果.于是对于输入的各种插入序列,你需要判断它们是否能生成一样的二叉搜索树. 输入格式: 输入包含若干组测试数据.每组数据的第1行给出两个正整数N (≤10)和L,分别是每个序列插入元素的个数和需要检查的序列个数.第2行给出N个以空格分隔的正整数,作为初始插入序列.最后L行,每行给出N个插入的元素,属于

二叉搜索树建立、插入、删除、前继节点、后继节点之c++实现

一.前言 一直以来,都对树有关的东西望而却步.以前每次说要看一看,都因为惰性,时间就那么荒废掉了.今天下个决心,决定好好的数据结构中的东西看一下.不知道看这篇文章的你,是不是和我有同样的感受,空有一颗努力的心,却迟迟没有付出行动.如果是的话,如果也想好好的把树的知识巩固一下的话,就让我们一起好好儿地把知识点过一遍吧.本文争取让看完的每一个没有基础的同学,都能有所收获.在正文开始前,先给自己加个油.加油(^ω^) 二.二叉搜索树的定义 二叉搜索树是指,对于某一个节点而言,它左边的节点都小于或等于它

剑指offer:二叉搜索树与双向链表

1.题目描述: 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表.要求不能创建任何新的结点,只能调整树中结点指针的指向. 2.解题思路: (1)将左子树构造成双向链表,并返回链表头节点: (2)定位左子树双链表的尾节点: (3)如果左子树链表不为空,将当前root连缀其链尾: (4)将右子树构造出双向链表,并返回链表头节点: (5)如果右子树链表不为空,将当前root连缀其表头: (6)根据左子树链表是否为空,确定返回的节点. 3.JavaScript实现: function Conv

数据结构——二叉搜索树、B树、B-树

数据结构——二叉搜索树.B树.B-树 1. 综述 二叉排序树(Binary Sort Tree),又叫二叉查找树(Binary Search Tree),也叫二叉排序树. 二叉搜索树满足以下性质: 1. 若根节点左子树不为空,则左子树上的所有节点均小于根节点: 2. 若根节点右子树不为空,则右子树上的所有节点均大于根节点: 3. 其左右子树也是二叉搜索树(递归定义): 4. 没有键值相等的点. B树就是B-树.B树/B-树英文叫B-Tree,可能被不小心翻译成了B-树.

PAT天梯赛练习题 L3-010. 是否完全二叉搜索树(完全二叉树的判断)

L3-010. 是否完全二叉搜索树 时间限制 400 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 陈越 将一系列给定数字顺序插入一个初始为空的二叉搜索树(定义为左子树键值大,右子树键值小),你需要判断最后的树是否一棵完全二叉树,并且给出其层序遍历的结果. 输入格式: 输入第一行给出一个不超过20的正整数N:第二行给出N个互不相同的正整数,其间以空格分隔. 输出格式: 将输入的N个正整数顺序插入一个初始为空的二叉搜索树.在第一行中输出结果树的层序

Java数据结构之二叉搜索树

Java数据结构之二叉搜索树 1.二叉搜索树组成 二叉搜索树又称为二叉排序树,它或者是一颗空树,或者是一颗具有如下特性的非空二叉树,需要满足一下三个条件: (1)若它的左子树非空,则左子树上所有结点的关键字均小于根结点的关键字: (2)若它的右子树非空,则右子树上所有结点的关键字均大于(可以等于)根结点的关键字. (3)左子树右子树本身又各是一颗二叉搜索树 在算法描述中,均以结点值的比较来代表其关键字的比较,因为若结点的值为类类型时,该类必须实现系统提供的java.lang.comparable