数据结构学习笔记(七)搜索结构

2023-12-19 21:16:18


1. 前言

本系列笔记基于 清华大学出版社的《数据结构:用面向对象方法与C++语言描述》第二版进行学习。

2. 概念

使用基于关键码的搜索,搜索结果应该是唯一的。使用基于属性的搜索方法,搜索结果可能不唯一。
搜索的环境有两种,静态环境和动态环境。区别在于插入和删除后搜索结构会不会变化。
衡量搜索的时间效率的标准是:
在搜索过程中关键码的平均比较次数或平均读写磁盘次数,成为平均搜索长度ASL。

3 静态搜索结构

3.1 静态搜索表

最简单的基于数组的数据表类。

#include <iostream>
#include<assert.h>
using namespace std;
const int defaultSize = 100;
class dataList;

class dataNode {
    friend class dataList;
public:
    dataNode(const int x = 99):key(x),data(0){}
    int getKey() const { return key; }
    void setKey(int k) { key = k; }
private:
    int key;                  // 关键码
    int data;
};

class dataList {
public:
    dataList(int sz = defaultSize) :ArraySize(sz), CurrentSize(0) {
        Element = new dataNode[sz];
        assert(Element != NULL);
    }
    dataList(dataList& R);
    virtual ~dataList() { delete[]Element; }                    
    virtual int Length() { return CurrentSize; }               
    virtual int getKey(int i)const {
    // 提取第i个元素值(从1开始)
        assert(i > 0 || i <= CurrentSize);
        return Element[i - 1].key;
    }         
    virtual void setKey(int x, int i) {         // 修改第i个元素的值
        assert(i > 0 || i <= CurrentSize);
        Element[i - 1].key = x;
    }
    virtual int SeqSearch(const int x) const;       // 搜索
    virtual bool Insert(int& e1);                       // 插入
    virtual bool Remove(const int x, int& e1);      // 删除
    friend ostream& operator<<(ostream& out, const dataList& OutList);
    friend ostream& operator>>(istream in, dataList& InList);
protected:
    dataNode* Element;
    int ArraySize;
    int CurrentSize;
};

class searchList :public dataList {
public:
    searchList(int sz = defaultSize) :dataList(sz){}
    virtual int SeqSearch(const int x)const;
};

int main()
{
    std::cout << "Hello World!\n";
}

bool dataList::Insert(int& e1)
{
    if (CurrentSize == ArraySize) return false;   // 表满,无法插入
    Element[CurrentSize] = e1;
    CurrentSize++;
    return true;
}

bool dataList::Remove(const int x, int& e1)
{
    if (CurrentSize == 0)return false;
    int i;
    for (i = 0; i < CurrentSize && Element[i].key != x; i++);
    if (i == CurrentSize) return false;
    e1 = Element[i].data;
    Element[i] = Element[CurrentSize - 1];
    CurrentSize--; return true;
}

ostream& operator<<(ostream& out, const dataList& OutList)
{
    cout << "Array Contents:\n";
    for (int i = 1; i < OutList.CurrentSize; i++) {
        cout << OutList.CurrentSize; 
        i++;
    }
    cout << endl;
    cout << "Array Current Size:" << OutList.CurrentSize << endl;
    return out;
}

ostream& operator>>(istream in, dataList& InList)
{
    cout << "Enter array Current Size:" << endl;
    in >> InList.CurrentSize;
    cout << "Enter array Elements:\n";
    for (int i = 1; i <= InList.CurrentSize; i++) {
        cout << "Element" << i << ":";
        in >> InList.Element[i - 1];
    }
    return in;
}

编译不过,因为Elements中没有设置接触到数据的函数,可以自行添加

3.2 顺序搜索表

int searchList::SeqSearch1(const int x, int loc) const
{
    if (loc > CurrentSize) return 0;
    else if (Element[loc - 1].key == x) return loc;
    else return SeqSearch1(x, loc + 1);
}

3.2.1 基于有序顺序表和顺序搜索和折半搜索

①顺序搜索

class SortedList : public searchList {
public:
    SortedList(int sz=100):searchList(sz){}
    ~SortedList(){}
    int SequentSearch(const int x)const;            // 顺序搜索
    int BinarySearch(const int x)const;                // 折半搜索
    bool Insert(const int& e1);                            // 插入
    int Begin() { return (CurrentSize == 0) ? 0 : 1; }      // 定位第一个
    int Next(int i) { return(i >= 1 && i <= CurrentSize) ? i + 1 : 0; } // 定位下一个
};

int SortedList::SequentSearch(const int x) const
{
    for (int i = 1; i <= CurrentSize; i++)
        if (Element[i - 1].key == x) return i;            // 成功,停止搜索
        else if (Element[i - 1].key > x)break;          // 不成功,停止搜索
    return 0;
}

②折半搜索
折半搜索又称二分搜索法
在这里插入图片描述
在这里插入图片描述
非递归算法

int SortedList::BinarySearch(const int x) const
{
    // 非递归算法
    int high = CurrentSize - 1;
    int low = 0;
    int mid;

    while (low <=high)          // 如果没有,最后搜索的一次是low<=high
    {
        mid = (low + high) / 2;
        if (x > Element[mid].key) low = mid + 1;            // 把中间的元素的后一位变成low
        else if (x < Element[mid].key) high = mid - 1;      // 把中间元素的前一位变成high
        else return mid + 1;
    }

    return 0;
}


递归算法

int SortedList::BinarySearch1(const int x, int low, int high) const
{
    int mid = 0;
    if (low <= high) {
        mid = (low + high) / 2;
        if (x > Element[mid - 1].key)
            mid = BinarySearch1(x, mid + 1, high);          
        else if (x < Element[mid - 1].key)
            mid = BinarySearch1(x, low, mid - 1);
    }
    return mid;
}

③插入算法

void SortedList::Insert(const dataNode& e1)
{
    assert(CurrentSize == ArraySize);
    int i = 1;
    while (i <= CurrentSize && Element[i - 1].key <= e1.key)i++;            // 找到小于插入的结点的最大的结点
    for (int j = CurrentSize; j >= i; j--) Element[j] = Element[j - 1];          // 前面的赋值给另一个
    Element[i - 1] = e1;
    CurrentSize ++;
}

4 二叉搜索树

在这里插入图片描述
简单来说,就是如果找到二叉树的中序遍历,他的中序遍历一定是从小到大排序的
在这里插入图片描述
如上图,其中序遍历均位123

4.1 搜索二叉树的类定义

struct BSTNode
{
    int data;               // 二叉树节点类
    BSTNode* left, * right; // 左右子女
    BSTNode():left(NULL),right(NULL){}
    BSTNode(const int d, BSTNode* L = NULL, BSTNode*R =NULL):data(d),left(L),right(R){}
    ~BSTNode(){}
    void SetData(int d) { data = d; }
    int getData() { return data; }
};

class BST {
public:
    BST() :root(NULL) {}                                                
    BST(int Value);                               
    ~BST() {};
    bool Search(const int x) {
        return (search(x, root) != NULL) ? true : false;
    }
    BST& oprator = (const BST & R);         // 赋值
    void makeEmpty() { makeEmpty(root); root = NULL; }
    void PrintTree()const { PrintTree(root); }
    int Min() { return Min(root)->data; }
    int Max() { return Max(root)->data; }
    bool Insert(const int& e1) { return Insert(e1, root); }
    bool Remove(const int x) { return Remove(x, root); }

private:
    BSTNode* root;
    int RefValue;               // 停止标志
    BSTNode* search(const int x, BSTNode* ptr);         // 搜索
    void makeEmpty(BSTNode*& ptr);                          // 置空
    void PrintTree(BSTNode* ptr)const;                          // 打印
    BSTNode* Copy(const BSTNode* ptr);                      // 复制
    BSTNode* Min(BSTNode* ptr)const;                                  // 求最小
    BSTNode* Max(BSTNode* ptr)const;                                 // 求最大
    bool Insert(const int& e1, BSTNode*& ptr);                      // 递归插入
    bool Remove(const int x, BSTNode*& ptr);                        // 删除
};

4.2 搜索二叉树的搜索

在这里插入图片描述
超级好理解

BSTNode* BST::search(const int x, BSTNode* ptr)
{
    if (ptr == NULL) return NULL;
    else if (x < ptr->data) return search(x, ptr->left);
    else if (x > ptr->data) return search(x, ptr->right);
    else return ptr;
}

4.3 搜索二叉树的插入

在这里插入图片描述

bool BST::Insert(const int& e1, BSTNode*& ptr)
{
    if (ptr == NULL) {
        ptr = new BSTNode(e1);
        if (ptr = NULL) { cout << "out of space" << endl; exit(1); }
    }
    else if (e1 < ptr->data) Insert(e1, ptr->left);         // 比结点小存左边
    else if (e1 > ptr->data)Insert(e1, ptr->right);   // 比节点小存右边
    else return false;              
}

4.4 搜索二叉树的删除

在这里插入图片描述

bool BST::Remove(const int x, BSTNode*& ptr)
{
    BSTNode* temp;
    if (ptr != NULL) {
        if (x < ptr->data) Remove(x, ptr->left);            // 左子树执行删除
        else if (x > ptr->data) Remove(x, ptr->right);    // 在右子树执行删除
        else if (ptr->left != NULL && ptr->right != NULL) { // 有两个子女节点
            temp = ptr->right;                                              // 右子树搜寻中序的第一个结点
            while (temp->left != NULL) temp = temp->left;
            ptr->data = temp->data;     // 用该节点数据代替根节点数据
            Remove(ptr->data, ptr->right);
        }
        else {      // 只有一个子女结点(左子结点或右子结点)           这部分是为了链接删除部分缺掉的那条线
            temp = ptr;
            if (ptr->left == NULL) ptr = ptr->right;
            else ptr = ptr->left;
            delete temp;
            return true;
        }
        }
    return false;
    }

5 AVL树

任一结点的右子树的高度减去左子树的高度差只能为-1,0,1

在这里插入图片描述

5.1 平衡化旋转

使不平衡的二叉搜索树变得平衡
先说插入
这里有四种情况

5.1.1 右旋:LL型状态

在这里插入图片描述
就是插入根节点的子结点的左子结点,
右旋相当于把树往顺时针旋转以下,把B点做成根结点,再把B结点的右子结点变成原根节点的左子节点,

旋转后变成:
在这里插入图片描述

5.1.2 左旋:RR型状态

左旋和右旋差不多,就是插入了根节点的右子结点的右子树中。
在这里插入图片描述
左旋相当于将整个树逆时针旋转了一下,将C点置为根结点,然后将A点作为C点的左子树,把原来的左子树3作为原根节点的右子树
在这里插入图片描述

5.1.3 右旋(LL)的例子

在这里插入图片描述
本次是插入0到原来的平衡树中,如图,属于LL情况,所以进行右旋操作。
找到不平衡的根结点4。所以我们要调整的树就是
在这里插入图片描述
将原有的左子树,以2为根节点的树往上提,作为这棵树的新根节点,并把原有根节点4作为2的右子树,把2的原右子变为原根节点4的左子树
在这里插入图片描述
再把2作为原来根节点4,进行连接
在这里插入图片描述

5.1.4 先左旋再右旋(LR)的操作

在这里插入图片描述

元素落到56都是一样的处理,把
D转换成新的根结点,把原有不平衡树的根节点A的左子女结点作为D的左子女结点,根节点A作为D的右子女结点。并把D的左子树当作B的右子树,把D的右子树当作A的左子树。
在这里插入图片描述

5.1.5 先右旋再左旋(RL)的操作

在这里插入图片描述

插入56所作的操作都一样,原理与LR差不多,把D作为新的根节点,把不平衡树的根节点A作为D的左子树,A的右子女结点C作为D的右子树。并把D的左子树转换成A的右子树,D的右子树转换为C的左子树。
在这里插入图片描述

5.1.6 RL操作的例子

在这里插入图片描述
把4当作新父结点,将不平衡树的根节点2作为4的左子树,根节点的右结点5作为4的右子树。把4的左子树赋给2作为其右子女结点。

在这里插入图片描述
结果
在这里插入图片描述

5.1.7 代码实现

void AVLTree::RotateL(AVLNode*& ptr)
{
    // 右子树高RR型状态
    AVLNode* subL = ptr;        // 要旋转的结点,即不平衡的树的根结点
    ptr = subL->right;              // 原根节点的右结点
    subL->right = ptr->left;     // 原根子树的右子女转化成其右结点的左子树
    ptr->left = subL;                 // 把原根节点作为原根节点右结点的左子树,此时ptr为新根节点
    ptr->bf = subL->bf = 0;

}

void AVLTree::RotateR(AVLNode*& ptr)
{
    // 左子树高LL型状态
    AVLNode* subR = ptr;
    ptr = subR->left;
    subR->left = ptr->right;
    ptr->right = subR;
    ptr->bf = subR->bf = 0;
}
void AVLTree::RotateRL(AVLNode*& ptr)
{
    AVLNode* subL = ptr;                // 最后根节点的左子树
    AVLNode* subR = subL->right;    // 最后根节点的右子树
    ptr = subR->left;                           
    subR->left = ptr->right;                // subR的左子树变为其左子女节点的左子女节点
    ptr->right = subR;                          // 新根节点的右子女结点变为最终右子女结点
    if (ptr->bf >= 0) subR->bf = 0;
    else subR->bf = 1;
    subL->right = ptr->left;                // subL的右子女变为新根节点的左子女结点
    ptr->left = subL;                           // 新根结点的变为最终的左子女结点
    if (ptr->bf == 1) subL->bf = -1;
    else subL->bf = 0;
    ptr->bf = 0;
}

void AVLTree::RotateLR(AVLNode*& ptr)
{
    AVLNode* subR = ptr;
    AVLNode* subL = subR->left;
    ptr = subL->right;
    subL->right = ptr->left;
    ptr->left = subL;
    if (ptr->bf <= 0) subL->bf = 0;
    else subL->bf = -1;
    subR->left = ptr->right;
    ptr->right = subR;
    if (ptr->bf = -1) subR->bf = 1;
    else subR->bf = 0;
    ptr->bf = 0;
}

5.2 AVL树的插入

沿着其插入路线查各结点的平衡度
各结点会有三种情况
①bf = 0,不需要处理,结束平衡化
②bf = 1,不需要处理,继续回溯
③bf = 2,bf = 2,右子树高,需要结合右子女q的bf做处理
1)q的bf = 1,执行左单旋转,插入位置为RR
2)q的bf=-1,执行先右后左旋转,插入位置为RL
bf=-2,左子树高,需要结合左子女q的bf做处理
1)q的bf=1,执行右单旋转,插入位置为LL
2) q的bf=-1,执行先右旋后左旋,插入位置为LR

bool AVLTree::Insert(AVLNode*& ptr, int& e1)
{
    AVLNode* pr = NULL;
    AVLNode* p = ptr;
    AVLNode* q;
    int d;

    Stack st;
    while (p!=NULL)
    {
        if (e1 == p->data) return false;        // 找到了e1的结点,不插入
        pr = p;
        st.push(pr);                                        // 用栈记录查找路径,并找到插入的父结点pr
        if (e1 < p->data) p = p->left;
        else p = p->right;
    }

    p = new AVLNode(e1);
    if (p == NULL) { cout << "error when allocate memory!"; exit(1); }
    if (pr == NULL) { ptr = p; return true; }
    if (e1 < pr->data) pr->left = p;
    else pr->right = p;                             // 新结点插入
    while (st.Isempty() == false) {         // 重新平衡化
        st.Pop(pr);                                 // 调查父结点的平衡因子 
        if (p == pr->left) pr->bf--;        // 如果插入左边,则平衡因子-1
        else pr->bf++;                          // 插入右边则+1,平衡因子高度右边-左边
        if (pr->bf == 0) break;              // 第一种情况,平衡退出
        if (pr->bf == 1 || pr->bf == -1)
            p = pr;                                     // 第二种情况,继续向上搜寻
        else {                                              // 第三种情况,|bf| =2;
            d = (pr->bf < 0) ? -1 : 1;          // 区别单双旋
            if (p->bf == d) {                       // 两节点的平衡因子同号,单旋转
                if (d == -1) RotateR(pr);       // bf ==-1
                else RotateL(pr);                   // bf =1
            }
            else {
                if (d == -1) RotateLR(pr);
                else RotateRL(pr);
            }
            break;

        }
        if (st.IsEmpty() == true) ptr = pr;     // 调整到树的根节点
        else {
            st.getTop(q);
            if (q->data > pr->data) p->left = pr;
            else q->right = pr;
        }

    }

    return true;
}

这是>>和<<的重载:

istream& operator>>(istream in, AVLTree& Tree)
{
    int item;
    cout << "Construct AVL tree:\n";
    cout << "Input Data(end with" << Tree.RefValue << "):";
    in >> item;
    while (item != Tree.RefValue) {
        Tree.Insert(item);
        cout << "Input Data(end with" << Tree.RefValue << "):";
        in >> item;
    }
    return in;
}

ostream& operator<<(ostream& out, const AVLTree& Tree)
{
    out << "Inorder Traversal of AVL tree.\n";
    Tree.Traverse(Tree.root, out);              // 以中序次序输出个各节点的数据
    out << endl;
    return out;
}

在这里插入图片描述
看懂这个基本就学会了插入了

5.3 AVL树的删除

AVL树的删除算法与二叉搜索树类似,删除后如果破坏平衡性质,还需要做旋转
(1)如果被删结点p有两个子女,p的中序次序下找到直接前驱q,把q的内容传给p,把结点q当作被删结点p,他是只有一个子女的结点,此时看第二种情况
(2)如果被删结点p最多只有一个子女q。可以当p的父结点pr中原来指向p的指针改到q,如果结点p没有子女,p父结点pr的相应指针置位NULL。将原来的结点pr为根的子树的高度-1,并沿pr通向根的路径考量一路上各个结点的影响。
考查结点q的父结点pr。如果q是pr的左子女,则因子bf+1,否则bf-1
此时有三种情况
【1】bf的原来的平衡因子为0,则不需要调整了,结束重新平衡
在这里插入图片描述

【2】bf不为0,需要考察结点pr的父结点的平衡状态,
在这里插入图片描述
【3】需要平衡化
考察父结点pr的更高的子树的根为q(未被缩短的树),根据q的平衡因子,有三种操作
①如果q的平衡因子为0,执行一个单旋(未被缩短的树,结束后可以结束平衡的过程
在这里插入图片描述
②如果q的平衡因子与pr的平衡因子正负号相同,则执行一个单旋来恢复,结束后需要沿上继续评估平衡状态,相当LL型和RR型
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
③如果pr与q的平衡因子正负号相反,则需要执行双旋q,相当于LR和RL型,同时继续向上评估
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

bool AVLTree::Remove(AVLNode*& ptr, int x, int& e1)
{
    // 删除关键码为x的结点
    AVLNode* pr = NULL;
    AVLNode *p = ptr;
    AVLNode* q;
    AVLNode* ppr;
    int d;
    int dd = 0;

    Stack* st;

    while (p != NULL) {
        // 寻找删除位置
        if (x == p->data) break;
        pr = p;
        st.push(pr);
        if (x < p->data) p = p->left;
        else p = p->right;
    }
    if (p == NULL) return false;            // 未找到,删除
    if (p->left != NULL && p->right != NULL) {  // 有两各子女的情况
        pr = p;
        st.push(pr);
        q = p->left;                // 再p左子树找到中序遍历直接前驱
        while (q->right != NULL) {
            pr = q;
            st.push(pr); 
            q = q->right;      
        }
        p->data = q->data;                  // 用q的值填补p
        p = q;                                      // 被删结点转为q,此时是只有一个子女结点q的情况
    }


    if (p->left != NULL) q = p->left;       // 被删结点只有一个子女
    else q = p->right;                              // 找到这个子女
    if (pr == NULL) ptr = q;    // 被删结点为根节点
    else {                                  // 被删结点不是根节点
        if (pr->left == p) pr->left = q;        // 把父结点连接到被删结点的子女结点
        else pr->right = q;
        while (st.IsEmpty()== false)            // 重新平衡化
        {
            st.Pop(pr);             
            if (pr->right == q) pr->bf--;       // 调整父结点的平衡因子
            else pr->bf++;
            if (st.IsEmpty() == false) {
                st.getTop(ppr);
                dd = (ppr->left == pr) ? -1 : 1;        // 旋转后和上层连接方向
            }
            else dd = 0;                                          // 旋转后不与上层连接
            if (pr->bf == 1 || pr->bf == -1) break;     // 不需要旋转,直接退出
            if (pr->bf != 0) {
                if (pr->bf < 0) { d = -1; q = pr->left; }
                else { d = 1; q = pr->right; }
                if (q->bf == 0) {
                    if (d == -1) {
                        RotateR(pr);
                        pr->bf = 1;
                        pr->left->bf = -1;
                    }
                    else {
                        RotateL(pr);
                        pr->bf = -1;
                        pr->right->bf = 1;
                    }
                    break;

                }
                if (q->bf == d) {
                    if (d == -1) RotateR(pr);
                    else RotateL(pr);
                }
                else {
                    if (d == -1) RotateLR(pr);
                    else RotateRL(pr);
                }
                if (dd == -1) ppr->left = pr;
                else if (dd == 1) ppr->right = pr;

            }
            q = pr;


        }
        if (st.IsEmpty() == true) ptr = pr;


    }

    delete p;

    return true;
}

不懂这段代码,真的很痛苦看的我

6 伸展树

每次插入结点,所有结点都要调整到根结点。
伸展树

6.1 插入

插入位置为根的左节点,右旋
插入位置为根的右结点,左旋
最左边LL情况,对中间结点进行右旋,在对结点进行右旋,直到变成根节点
最右边RR情况,对中间结点进行左旋,在对结点进行左旋,直到变成根节点
LR情况,中间结点先左旋后右旋,再对结点左旋右旋,直到变为根节点
RL情况,中间结点先右旋后左旋,再对结点右旋左旋,直到变为根节点

6.2 查找

插入和查找操作,查找后把查找到的结点变为根节点,情况跟插入类似

6.3 删除

和二叉搜索树相同,但需要把被删除结点的父结点展开到根节点。

7 红黑树

在这里插入图片描述
1)外部结点是黑色(空指针们
2)没有两个连续结点是红色,(可以两个连续结点是黑色
3)根到外部节点的路径上都有相同的黑色结点
在这里插入图片描述
也可以从指针看
在这里插入图片描述

在这里插入图片描述

7.1 红黑树搜索

和二叉搜索树完全相同

7.2 插入和删除

这个书上标**了,我这边先不学,看了下内容挺多的,后面再补

文章来源:https://blog.csdn.net/Lmz_0314/article/details/134953042
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。