4.2-数据结构-基础知识

279 阅读40分钟

hello data

总揽:

是什么:

  • 前人总结的某些特定问题的解决方案,一组有特定关系的数据的存储处理
  • 实际问题具体抽象成计算机能处理的基本形式。
数据的逻辑结构

所有的数据都可以归纳为这些结构。? container-存储某种逻辑关系:

  • 集合结构:元素之间没有关系
  • 线性结构:元素之间有排序的情况
  • 树形结构:元素之间有层次的关系。除了根结点没有前驱结点!每个结点只有一个前驱节点,后驱不要求
  • 图形结构:描述元素之间的连接关系。可以多前驱多后驱

屏幕快照 2021-11-30 下午2.44.03.png

屏幕快照 2021-11-30 下午2.44.03.png

屏幕快照 2021-11-30 下午2.44.03.png

屏幕快照 2021-11-30 下午2.44.03.png

数据结构的操作
  • 创建
  • 清除
  • 插入
  • 删除
  • 搜索:搜索是不是存在,某个特定条件
  • 更新
  • 访问:元素的值
  • 遍历:每个元素只访问一遍。
  • 其他操作。
数据有哪些存储的关系与怎样运算

存储结点存储元素,存储结点之间的关系就是数据之间元素的关系,也就是在计算机里安家居住,找到各个结点之间的物理位置之间的关系。此外还有各个数据结构特殊的信息。

数据之间的关系:

  • 顺序存储:各个元素的地址已知。主要用数组。

  • 链接存储:存储别的元素的地址,用指针显式表示元素之间的关系。

  • 哈希存储:存放的数组的下标之间和对应的数组的key关键字之间建立关系,哈希函数对key进行函数以后可以得到其对应的下标。 H(key)=iH(key)=i,所以我们可以通过搜索键值就找到数组的下标,那么其对应的值就直接可以得到,因为数组通过头指针和下标可以轻而易举找到元素对应的位置。 主要用于集合内元素没有关系的结构,方便查找。

  • 索引存储:一个专门存储数据结点,一个索引区存储数据结点之间的关系,也就是数据的地址簿,先访问索引区就可以找到对应元素的地址,然后根据地址直接在数据区找到元素。没见过这种数据结构

运算实现,用函数操作

  • 每个运算对应一个算法
  • 每个算法用一个函数表示(系统自带的函数吗?)
  • 每个数据结构有一组函数??
每个数据结构多种算法其性能指标:
  1. 算法运行时间:忽略计算机的硬件性能,忽略处理具体问题的规模,忽略具体问题的难度也就是数据分布,忽略编译语言也就是计算机执行语言的长处和短处。 所以我们以标准操作运算量和数据规模之间的关系。同时考虑数据分布的具体情况:
  • 时间复杂度 - 最好情况的时间复杂度 - 最坏情况的时间复杂度用得最多 - 平均情况的时间复杂度 按照数据出现的概率和实即运算时间的总影响来评价。

1.2. 大O表示法(上界) 直观事实上的运算量:先确定一个标准操作的类型,然后计算算法操作的总操作数量。 用于算法比较的抽象运算量:由于每一个操作时间很短,所以不考虑小规模问题,我们假设数据都是大规模问题,n -++无穷所以只考虑数量级和数量级的上界。

  • 两种方式:求和定理和求积定理。
    • 找出运行时间最长的程序,也就是运行时间函数的上界,也就是运行时间的主项。 由于不同的运行时间函数有着不同的数量级,大O表示法的假设是运行数据规模很大,在此情况下不同数量级之间的取值相差是非常大的,小数量级的运行函数的值几乎可以忽略,所以我们可以取数量级最大的运行时间函数也就是运行时间函数的主项来表示时间复杂度。
    • 对于多重循环调用数量级相乘。
  1. 空间复杂度:
  • 算法处理过程中所需的额外工作量,也就是除了存储数据之外需要的空间。
  • 一般按最坏情况处理
  • 用大O表示法
最大子序列的和:

解题思路: - 对于一个子序列我们把该元素之前的所有子序列都排列出来,那么遍历每个元素都求出来该元素位置下最好的子序列和,最终得到理想的子序列和。 可以使用O(n2)O(n^2)复杂度的算法: - 在枚举法的基础上改进 - 现象:和为负的子序列不可能是最大连续子序列的开始部分 - 结论:当检测出一个负的子序列时,可以让start直接增加到j+1 优化程序

  1. 枚举起点中点方法:
    int maxSubsequenceSum(int a[], int size, int &start, int &end) { 
    int maxSum = 0; 
    // 枚举起点 i 
    for (int i = 0; i < size; i++ ) {
    // 枚举终点 j
        for( int j = i; j < size; j++ ) { 
            int thisSum = 0; 
            // 计算i到j的子序列和
            for( int k = i; k <= j; k++ ) thisSum += a[ k ];
            // 如果是最大的,存下这个子序列的起点和终点以及最大的和
                if( thisSum > maxSum ) { 
                    maxSum = thisSum; 
                    start = i; 
                    end = j;
                }
            }
        } 
        return maxSum; 
      }
  1. 枚举子序列:

//遍历每个元素找到最好的结果 在枚举的时候,ii 到 j+1j+1 这个子序列的和,没必要再用一个for循环,可以直接在 ii 到 jj 这个子序列的和上再加1个数,省略到最里层的循环。

int maxSubsequenceSum(int a[], int size, int &start, int &end) { 
    int maxSum = 0; 
    for (int i = 0; i < size; i++ ) {
        int thisSum = 0; 
        for( int j = i; j < size; j++ ) { 
            // 直接在之前的计算结果上加上一个数,就能得到i到j的子序列和 
            thisSum += a[ j ];
            if( thisSum > maxSum ) { 
                maxSum = thisSum; 
                start = i;
                end = j 
            } 
        } 
    } 
    return maxSum; 
}
  1. 分治法也叫二分法也叫递归法:

与分治排序法类似:

int maxSum(int a[ ], int left, int right , int &start, int &end) { 
    int maxLeft, maxRight, center; 
    int leftSum = 0, rightSum = 0; int maxLeftTmp = 0, maxRightTmp = 0; 
    int startL , startR, endL, endR; 
    // 递归的终止条件
    if (left == right) { 
        start = end = left; 
        return a[left] > 0 ? a[left] : 0;
    } 
    center = (left + right) / 2; 
    // 递归地计算整个位于前半部的最大连续子序列 
    maxLeft = maxSum(a, left, center, startL, endL); 
    // 递归地计算整个位于后半部的最大连续子序列
    maxRight = maxSum(a, center + 1, right, startR, endR);
    // 计算从前半部开始在后半部结束的最大连续子序列的和
    // 选择三个值中的最大值 
    if (maxLeft > maxRight ) 
        if (maxLeft > maxLeftTmp + maxRightTmp) { 
            start = startL; end = endL; return maxLeft; 
        } else return maxLeftTmp + maxRightTmp; 
        //完整的一条if语句无须加括号
    else if (maxRight > maxLeftTmp + maxRightTmp) { 
        start = startR; end = endR;  return maxRight;
        } else return maxLeftTmp + maxRightTmp; 
    }
  • 现象:和为负的子序列不可能是最大连续子序列的开始部分
  • 结论:当检测出一个负的子序列时,可以让start直接增加到j+1
int maxSubsequenceSum(int a[], int size, int &start, int &end) { 
// starttmp用于保存前面的最优方案
    int maxSum, starttmp, thisSum;  
    start = end = maxSum = starttmp = thisSum = 0; 
    for( int j = 0; j < size ; ++j ) { 
        thisSum += a[j];
        if ( thisSum <= 0 ) { 
            thisSum = 0; starttmp = j+1; 
        } else if (thisSum > maxSum ) { 
            maxSum = thisSum; start = starttmp; end = j; 
                    } 
        } 
    return maxSum; 
}

线性表结构:

线性关系:除了开头和结尾都有前驱和后继;

线性表:处理线性表元素 线性表的下标和数组一样从0开始;

操作:创建;清除;求长度;删除某个位置的元素;添加某个位置的元素;搜索元素是否在表内;找到表内的第i个元素;遍历线性表的元素; 以及其他可以抽象成线性表的内容;

数据结构都定成一个抽象类:都需要定义一个类模板参数;

析构函数将指针清零。

虚析构函数是为了解决父类指针指向子类对象时,释放子类对象的资源时,释放不完全,造成的内存泄漏问题。

- c++是一个强类型函数,必须定义参数的变量模型。数据结构可以存储多种数据类型,所以需要使用模板,但是当基类指针指向派生类对象,或者派生类对象生成派生类实例时,会无法使用派生类自己定义的成员函数,由于我们创建派生类对象就是为了要使用派生类对象的特性或者让派生类对象执行自己的任务,所以派生类必须实现自身的纯虚函数。
  • 创建纯虚函数可以直接在基类里面定义成员函数之前就加上vituralvitural

  • 如果想直接调用基函数也是可以将指针变成基类指针就可。

  • creat函数也就是创建对象的函数就变成类对象的创建也就是类的初始化。

  • delete[],为抽象类添加一个析构函数,尽管派生类看起来不存在需要析构的参数,该法会返回基类析构函数,由于基类也是虚的析构函数,会返回到派生类调用派生类的析构函数。

顺序表概念概念和定义:

顺序表包括地址和前驱和后继,首先其结点的物理地址是连续的。

图片 1.png

  • 顺序表开头元素的指针a
  • 顺序表的规模maxsize
  • 顺序表当前的长度也就是元素个数currentLength

顺序表需要实现插入或者删除操作: 对于数组我们需要实现动态数组,也就是数组长度可以变化:

顺序表的类模板
  1. 声明一个类模板 template<class elem Type> 顺序表必须实现所有数据类型元素的派生。只用了一个: class seqList: public list<elem Type> 模板类中定义数据的成员变量,一般是类中独有的所以设为private
private:
    elem Type *a;
    //pointer
    int maxsize;
     //数据表的容量也就是数据表的规模;
    int length;
    //数据表的表长也就是数组中元素的个数;
    
public:
    //first declaration:
    // construct function
    //seqList
    //结构函数:
    // 初始化一个长度
    //give all the member same value;   
    seqList(int initSize = 10);
    
    //析构函数没有添加而且虚函数也没有构造
    //delete数组使用delete[]
    ~seqList() {delete[] data; }
    
    //clear,why delete its data?
    void clear() { currentLength = 0;}
    
    //length
    int length() const{ return currentLength;}
    
    //visit,数组连续直接得到;
    elem Type visit(int i ) const{ return a[i];}
    
    //search
    int search (const elemType &x) const;
    // &x返回x变量的指针,这样不会改变他的元素value;
    //指针的知识,const &x;而且&x可以直接在类中使用变量名直接操作,否则需要使用变量的指针。或许是因为模板类都需要传入指针才可以使用。
    //引用的方式也是定义了一个指针指向模板类的某个元素;
    //最后使用一个const不太理解,发现模板类中返回一个值时需要在前面加上const;
    //according to the elem's value,find the index;
    
    //insert
    void insert(int i, const elemType &x);
    
    //void doubleSpace();
    void doubleSpace;
    
    //remove
    void remove(int i);
    
    //traverse
    void traverse() const;
    //directly conduction to the array;
        

顺序表存储容量不足

    void double Space();
    // 不想让外界访问所以私有
  • 由于顺序表的物理存储地址和算法中的逻辑下标一一对应,所以可以定位访问顺序表的元素的性能很好。因此顺序表适合存储静态数据和经常需要定位访问的情况;
  • 但是由于其物理地址相对比较固定,所以对于元素的添加和删除需要更改大量的元素比较费时间,这是它的不足;
  1. 定义模板类中的成员函数
  • 模板函数也可以在public中定义:
template<class elem Type>
seqList<elem Type> :: seqList(int initSize){
//构造函数和定义类时的
//class seqList<elem Type> :: public List<elem Type>
//很像,定义为Public以后,还需要传入元素的elem Type;也就是定义了一个基类叫做List而且基类的数据类型也是和类使用一样的模板吗?

    data = new elem Type[initSize];
    //这里我定义的是data -> a;
    maxSize = initSize;
    currentSize = 0;
    //没有添加元素时list的长度为0;
}
  • search函数:
template<class elem Type>
//differ 
//class seqList<> :: public List<>
int seqList<elem Tyoe> :: search(const elem Type &x)const{
    int i;
    for(i = 0; i < currentLength && a[i] != x; i++);
    // null conducting sentence;
    if(i == currentLength)return -1;
    else return i;
}
  • traverse函数:也就是输出数组元素的value;
template<class elem Type>
void seqList<elem Type> :: traverse()const{
    cout << endl;
    //格式优美;
    for(int i = 0; i < currentLength; i++)
        cout << a[i] << ' ';
}
  • insert函数:
    1. 首先给数据元素腾空间,也就是把插入位置后面的元素都向后移动,不会丢失数据的同时还腾出了空间。并且需要检查当前数据的规模maxSize是否能够容纳住这些元素。可以使用double Space;函数来增加数组的空间。
    2. 在指定位置插入元素;
    3. 更新顺序表的currentLength和maxSize;
template<class elem Type>
void seqList<elem Type> :: insert(int i, const elem Type &x){
    if(currentLength == maxSize){
        doubleSpace();
    }
    for(int j = currentLength; j >= i+1; j++){
        a[j] = a[j-1];
    }
    a[i] = x;
    currentLength++;
}
  • doubleSpace函数 由于语言不可以增加数组的长度,所以我们只能把数组转移到更大的全新的家里,也就是换成大房子。
template<class elem Type>
void seqList<elem Type> :: doubleSpace(){
    elem Type *tmp = a;
    //数组的头元素本身就能表示地址所以不需要&a;
    maxSize *= 2;
    //新家的房子的尺寸;
    //新家盖好了
    a = new elemType[maxSize];
    //不过 elem Type a[maxSize];不同
    //搬家;
    for(int i =0; i < currentLength; i++){
        a[i] = tmp[i];
    }
    //将老家卖掉给别人住
    //释放指针
    delete [] tmp;
}
  • remove函数 让i+1位置开始的元素到最后一个元素为止,所有元素向前移动一个位置;
template<class elem Type>
void seqList<elem Type> :: remove(int i){
    for(int j = i; i < currentLength - 1; i++){
        a[i] = a[i+1];
    }
    currentLength--;
    //!!!
}
  • 注意currentLength在遇到insert 和remove函数时都需要更改长度;

定义线性表的抽象类,类中的每个函数都是纯虚函数,在实现数据结构时需要去完成每一个纯虚函数。

  • 和线性表的操作相比,少了创建操作,因为这可以用每个类的构造函数来完成。
  • 多了虚析构函数,是为了防止内存泄漏,把申请的动态内存析构掉。
总结顺序表:
  • 由于要保持逻辑次序和物理次序的一致性,顺序表在插入删除时需要移动大量的数据,性能不太理想;
  • 由于逻辑次序和物理次序的一致性使得定位访问的性能很好
  • 顺序表比较适合静态的、经常做线性访问的线性表。

单链表和双链表

顺序表的链接实现,也就是不光存储了value还存储了后继结点之的地址。 头结点head不存储数据只存储后继第一个元素的地址。

  • 单链表:

图片 2.png 也就是一个结点有两个存储的元素分别是指针和元素; 代码请参考VSCODE

存储和操作:

  1. 存储的设计:
  • 数据成员1:头结点node自己定义需要建立一个结点类,struct没有指定访问权限都是public,而class内所有权限默认都是private;类需要封装,由于该类放在了单链表中的private中所以也不会被外界访问到;也就是可以更改该元素但是不可以直接访问,比如你让它输出它的后继元素的地址电脑就不会告诉我;

  • 数据成员2:此外还需要同时保存截止到该结点时该链表的长度;这里用currentLength表示。

  • 指针的特性: 头结点其是就是一个指针,头指针。确定线性表中第一个元素对应的存储位置。 单链表节点中指针的作用是指向当前节点的后驱节点,如果指针为空说明无后驱。 顺序表中存储数据的物理地址是连续的,故无需指针便可查找到后驱节点。 MY ANSWER: 保存后继元素的地址;顺序表的物理内存中的地址和他的索引下标是一致的,所以直接通过访问下标就可以找到该元素的地址。

  1. 操作的工具函数设计:
  • move找到索引为i的元素的地址;
  • search(x):搜索某个元素在线性表中是否出现。并返回索引,如找不到返回-1
  • visit(i):访问线性表的第i个元素。通过move(i)找到第i个结点,返回该结点的数据部分
  • traverse():遍历运算。从头结点的直接后继开始重复:输出当前结点值,将后继结点设为当前结点,直到当前节点为空。

单链表:普通操作

#include <iostream>

#define N 500010

using namespace std; 

int head[100010], nxt[N], val[N], out[N], cnt;

int main() {
    int n, m, t, x, y;
    cin >> n >> m;
    while (m--) {
        cin >> t;
        if (t == 1) {
            cin >> x >> y;
            val[++cnt] = y;
            nxt[cnt] = head[x];
            head[x] = cnt;
        }
        else if (t == 2) {
            cin >> x;
            head[x] = nxt[head[x]];
        }
        else {
            for (int i = 1; i <= n; i++) {
                int oc = 0, pos = head[i];
                while (pos) {
                    out[++oc] = val[pos];
                    pos = nxt[pos];
                }
                if (oc == 0)
                    cout << "none";
                while (oc)
                    cout << out[oc--] << " ";
                cout << endl;
            }
        }
    }
    return 0;
}

单链表VS双链表

当已知一个指针指向单链表的该结点时,我们可以获得该结点的地址,但是无法获得该结点前继结点的地址从而获得前继结点的`next`指针。
但是双链表获得当前结点的地址,当前结点可以存储一个前继结点的指针,从而获得前继结点的地址。

双链表

  • 不光有头结点也有尾结点,因此多了一个尾指针。

  • 双链表同样需要满足多种数据类型的需求,因此在线性表的基础上进行派生,由此是一个抽象类继承。

  • move函数可以添加用来增加函数。

  • 插入:如果在i处插入元素那么,应该找到i元素的位置在i元素之前插入该元素。

  • 删除:

#include <cstdio> 
#include <cstring> 
#include <iostream> 
#include <algorithm> 

#define N 100005 

using namespace std; 

struct node { 
    int l, r; 
}nod[N]; 
//nod[N]啥意思;创建一个这个结点的数组?

int head, tail; 
void add(int a, int b, int c) { 
    nod[a].r = b; 
    nod[b].r = c; 
    nod[c].l = b; 
    nod[b].l = a; 
} 

void del(int a, int b, int c) { 
    nod[a].r = c; 
    nod[c].l = a; 
    nod[b].r = nod[b].l = 0; 
    //指针都清零
} 

int n, m; 
int main() { 
    scanf("%d", &n); 
    head = n + 1;  
    tail = n + 2; 
    //?????
    
    nod[head].r = tail; 
    nod[tail].l = head; 
    add(head, 1, tail); 

    for (int i = 2; i <= n; ++i) { 
        int x, y; 
        scanf("%d%d", &x, &y); 
        if (!y) add(nod[x].l, i, x); 
        else add(x, i, nod[x].r); 
    } 
    
    scanf("%d", &m); 
    for (int i = 0; i < m; ++i) { 
        int x; 
        //每个循环都重新初始化
        scanf("%d", &x); 
        
        if (!nod[x].l) continue; 
        //继续删除下一个编号的学生;
        del(nod[x].l, x, nod[x].r); 
    } 

    for (int i = nod[head].r; i != tail; i = nod[i].r) printf("%d ", i); 
    //存在NOD里的数组是学生编号;
    return 0; 
}

栈(一种线性表)

顺序栈

顺序栈是一种能顺序实现的栈。

使用连续空间来存储栈的结点,即使用顺组存储栈的结点。

栈有以下性质:

  • 操作做了限定,“后进先出LIFO,先进后出FILO”
  • 也就是只有一端允许(不能任意)对数据存储和释放;栈顶是能够操作的 一端,栈底无法操作

由此,顺序栈中下标为0的一端为栈底。进栈和出战都是在栈顶操作,因此不会像数组一样删除添加元素时移动大量元素。

  1. 存储结构和操作:

    经典案例:一摞书,只能在最上面放书和拿书;

    • 创建一个空栈,create()
    • x表示一个栈??
    • 进栈后就变成了栈顶push(x)
    • 出栈只能出栈顶pop(x)
    • 访问也只能访问栈顶top(x)
    • 栈的长度和遍历以及查找都不可行??
    • 判断栈是否为空isEmpty(x)返回True/False
顺序栈的抽象类

虚析构函数:防止内存泄漏,使用该抽象类指针指向具体派生类实例时,可以把指针都释放。

  1. 栈的顺序实现,依靠的是连续空间的数组,顺序表: 栈的存储
    • 数组头指针
    • 数组的大小max_size:
    • 数组的栈顶位置cur_len(类似)
    • 注意和数组一样下标从0开始,表示栈底

也就是和数组类似,只是操作不同 栈的操作

- 构造函数:
`初始化首先初始化数组长度,然后top_p = -1表示栈是否为空,max_size = init_size`

- push(x)函数
将top_p加1,将x放入top_p指出的位置中。但要注意数组满的情况。
复杂度最好是O(1)最坏是O(N),所以复杂度均分到N-1之前的操作,也就是平均加1,因此复杂度是O(1)`++top_p = x`
- pop(x)
并将top_p减1。`top_p--`

- isEmpty():
若top_p的值为-1,返回true,否则返回false。构造函数初始化是-1

- top(x)函数
返回top_p指出的位置中的值

4. 算法的复杂度使用均摊分析法:

最坏情况在N次进栈操作中至多出现一次。如果把扩展数组规模所需的时间均摊到每个插入操作,每个插入只多了一个拷贝操作,因此从平均的意义上讲,插入运算还是常量的时间复杂度
链接栈
  • top_p在表头:和单链表类似,插入删除操作在表头比较方便。使用单链表就足够实现功能了

  • 所以不需要head只需要定义一个top_p就可,初始化为NULL可以用来判断空栈; -push(x) 头插法,先定义一个结点,结点指向表头,表头指针更新为当前结点。

  • 不需要doubleSpace

  • 两个指针,先保存要删除的结点,然后更新top_p,最后删除tmp;首先要判断当前top_p是否是NULL否则无法next

  • pop()函数:

  1. 返回栈顶data
  2. 删除当前栈顶指针:先保存top_p 的指针先保存,然后更新top_p,最后删除tmp;

时间复杂度为O(1)

递归函数的非递归实现:

main函数第一个语句,函数语句按顺序进行,符合“后进先出”。 一句一句实现,使用函数栈使程序自己管理自己的栈。

  • 定义一个字问题的程序栈,
  • 整个问题放入程序栈中 while(栈非空) 执行解决问题的过程,分解出的小问题进栈。

1638695022267.jpg 这里可能没有把当前运算的函数放进栈里。

快速排序非递归实现:

栈记录排序区间的数据,定义了一个结点,结点有两个元素分别是区间的左端点和右端点。

  • 第一部分的操作步骤:
先将整个数组进栈,然后重复下列工作,直到栈空:
    从栈中弹出一个元素,即一个排序区间。
    将排序区间分成两半。
    检查每一半,如果多于两个元素,则进栈。
  • 进栈先是小于mid 后是大于mid,
  • 大于mid: (mid+1 size -1)
  • 直到mid 和start之间没有元素mid == start(2) 或者是mid = start + 1(3)。 这个时候while循环结束。

divide直接求中间结点不分偏左还是偏右,唯一区别就是最后一个进栈的区间mid = start还是mid = finish。已经按照中点进行元素的分割。

void quicksort(int* array,int left,int right) { 
    stack<int> s; 
    s.push(left); 
    s.push(right); 
    while(!s.empty) { 
        int right = s.top(); 
        s.pop(); 
        int left = s.top();
        s.pop(); 
        int index = partsort(array,left,right); 
        if((index - 1) > left) { 
            s.push(left); 
            s.push(index - 1);
        } 
        if((index + 1) < right) { 
            s.push(index + 1); 
            s.push(right);
        } 
    }
}
int partsort(int *a,int l, int r){
    int pivot = a[r]; 
    // 元素移动
    int k = l - 1; 
    for (int j = l; j < r; ++j) if (a[j] < pivot) swap(a[j], a[++k]); 
    swap(a[r], a[++k]);
    int mid =  l + (r-l)/2;
    return mid;
}
// 每个node表示要对从left到right这一部分数据进行排序
struct  node {
    int left;
    int right;
};

void quicksort( int a[],  int size) { 
    seqStack <node> st;
    int mid, start, finish;
    node s;

    if (size <= 1) return;

    // 排序整个数组
    s.left = 0; 
    s.right = size - 1;
    st.push(s);
    while (!st.isEmpty())  {
        s = st.pop(); 
        start = s.left;
        finish = s.right; 
        mid = divide(a, start, finish);
        if (mid - start > 1) { 
            s.left = start; 
            s.right = mid - 1; 
            st.push(s); 
        }
        if (finish - mid > 1) { 
            s.left = mid + 1; 
            s.right = finish;
            st.push(s);
        }
    }
}
括号配对检查:
  • 检查对象:圆括号、方括号、花括号、单引号和双引号以及注释/.../

  • 如果只是检测单一的括号:A(B)

  • 但是多种括号就不能只用某种括号的开闭的数量和位置关系来确定是否匹配;

  • 在遇到第一个右括号之前,一直将左括号push,直到遇到一个右括号,左括号开始出栈,如果匹配左括号出栈。 遇到右括号时,需要与最近遇到的、没有匹配的左括号匹配。

    使用栈,遇到左括号进栈;见遇到右括号,则出栈,并比较两者是否配对。

stack只是存储某些元素。

具体算法:
1.首先创建一个空栈。

2.从源程序中读入符号。

3.如果读入的符号是开符号,将其进栈。

4.如果读入的符号是一个闭符号但栈是空的,出错。否则,将栈中的符号出栈。

5.如果出栈的符号和和读入的闭符号不匹配,出错。

6.继续从文件中读入下一个符号,非空则转向3,否则执行7。

7.如果栈非空,报告出错,否则括号配对成功。

具体算法: 首先,创建一个空栈 字符串读入一个符号 如果符号时开括号将其进栈 如果闭括号但栈是空的那么返回错误,否则将开括号出栈。 继续读直到遇到结束符 如果此时栈非空,输出错误;

如果用于成熟的真实C++场景还需要考虑其他情况:

至少需要考虑三种括号:圆括号、方括号和花括号。

如果括号出现在注释、字符串常量、字符常量中时,就不需要考虑。

匹配问题

在考虑单引号、双引号时,还必须考虑转义字符。
Balance类设计:

源文件名,读取源文件的数据,保留行号和列号; - 二维数组; 二维数组顺序遍历所有的字符

  • 括号配对检查
  • 输出不匹配出现错误的符号出现的行号和列号和错误数 其中错误符号出现的位置,需要先辨别是什么错误:
    1. 右括号和左括号不匹配,标记左括号还是右括号,标记最近的还是匹配所有以后决定的。
    2. 所有数据读取以后,栈非空,直接标记栈内的左括号;
    3. 由于面临经常性的出栈和入栈所以必须检查栈当遇到一个右括号时,如果是空,这时应该标记右括号。

设计考虑:

数据成员: - 需要检查的源文件名:文件流对象;不是根据文件名而是根据文件流对象; - 行号; - 错误数; 为了优化需要添加一个结构体包括错误类型和错误出现的行数,也就是具体信息 - 同时定义一个需要忽略的括号类型: //,/,..../

成员函数: - 构造函数;

  • 检查括号配对函数
  • 其他工具

??? 可以将数据成员 currentLine 和 Errors 作为 CheckBalance 的局部变量,并通过参数的形式传递给其他需要使用到的函数。

class balance { 
    private:
        ifstream fin;
        //putback(char)是文件流类型独有的成员函数吗?
        int currentLine; 
        int Errors;
        struct Symbol { 
            char Token; 
            //表示错误类型;
            int TheLine; 
            //在第几行
        }; 
        enum CommentType { SlashSlash, SlashStar }; 
    public:
        balance(const char *s);
        //默认输入字符串
        int CheckBalance(); 
        //主函数
        
    privatebool CheckMatch(char Symb1, char Symb2, int Line1, int Line2 ) ; 
        //是否匹配
        
        char GetNextSymbol();
        //直到读到一个有效的符号;
        
        void PutBackChar(char ch); 
        //输出出错的括号
        void SkipComment(enum CommentType type); 
        //跳过slash slash 和slash star
        void SkipQuote(char type); 
        //跳过字符常量`'\'' `,`""`
        char NextChar();
        //读入字符时处理行号
};

成员函数构造: 第1项工作: GetNextSymbol

功能:从文件的当前位置开始,跳过所有的非括号,读到第一个括号后返回。在遇到文件结束时,返回一个特定的符号,如NULL。

函数原型:函数有一个字符型的返回值。执行该函数必须有一个输入流,在读输入流的过程中,当前处理的行号会变,在读的过程中也可能遇到异常情况,因此出错数也会变。这三个信息:文件流对象,当前处理的行号,出错个数都是对象的数据成员。因此函数原型为:char GetNextSymbol()。

特殊情况:

注释中的括号不用考虑

字符串常量和字符常量中的括号不用考虑。
  • C++中的注释又有两种形式

    以“//”开始到本行结束

    以“/”开始到“/”结束,可以跨行

**此时enum CommentType { SlashSlash, SlashStar }; **

给node结点赋值需要使用{}只有new 时需要圆括号

转义字符可以处理: 必须找到成对, 跳过注释: 遇到换行符跳过当前注释

第3项工作:CheckMatch 功能:比较两个指定位置的待比较的符号是否匹配 函数原型:bool balance::CheckMatch(char Symb1, char Symb2, int Line1, int Line2 )

后缀表达式:

中缀表达式:

- 带有括号的两个数之间是运算符的表达式;优先级,从左往右,括号;

后缀表达式:

- 基本思想:
    先扫描到运算符,找到离运算符最近的两个数进行操作;

image.png

中缀表达式转成后缀表达式: 使用一个栈来保存当前表达式还无法运算的运算符

- 遇到运算法,取决于后面的运算符的优先级,但是可以决定前面的运算符;
- 如果输入的是左括号那么进栈
- 如果输入的是右括号那么从右括号左边的运算符开始出栈,一直遇到左括号;
- 如果输入的是运算符首先看前面是否还有运算符并且此时元素还没有出栈完全,如果栈顶的运算符级别比较高或者是平级,那么栈顶可以出栈;如果是比较低的运算符,那么该元素进栈。
- 最后元素都没有了开始把所有的运算符都出栈,其中左右括号不进栈。
计算器类的实现:
  • 运算符:+-*/^

模板类传递参数如果是多个参数可以在定义对象时,使用()或者定义对象以后,使用{}给元素赋值。单个元素可以直接赋值。 enum枚举类型

可以将转换和计算两个步骤合并起来,边转换边计算。同时使用两个栈。 运算数栈和运算符栈;最后结果也在运算数栈中; e8cNj1SDLWk-rOrfrvbiO.jpg

出错信息: 运算数 枚举类一次产出一个 最后输入的str要将expression返回。不改变原来表达式?

MF7w8VNQnkDfJx-tIWqyA.jpg

弹栈之前先确定栈是否是非空。 while(*expression && *expression == ' ') ++expression ; 对字符进行比较运算时需要先确定该元素非空字符才可以比较;而且&&具有短路特性也就是说前半句如果错误后半句就不会执行,所以需要把判断该元素是否是飞空作为前提条件。

队列:

顺序队列:
  • 只能从对队尾入队,从队头出队。

队列的基本操作

  • 创建一个队列create():创建一个空的队列;
  • 入队enQueue(x):将x插入队尾,使之成为队尾元素;
  • 出队deQueue()删除队头元素并返回队头元素值;
  • 读队头元素getHead():返回队头元素的值;
  • 判队列空isEmpty():若队列为空,返回true,否则返回false

插入删除和访问: first in first out 插入和删除不在同一个头部;

顺序队列的三种组织方式:

1.队头位置固定:  头元素下标为0,并指定尾元素,当队列为空时尾元素被指定为-1.

2.队头位置不固定:  队首指针front指示队首结点的前一位置,队尾指针rear指示队尾结点存放的下标地址,初始化将两指针均设为-1,队满时rear=Maxsize-1.

3.循环队列 :  依旧一个front和一个rear,牺牲一个单元规定front指向的单元不能存储队列元素,队列满的条件是:(rear+1)%MaxSize == front。初始化两个指针都指向0

qIGU-nD2FuRVQFfoADg_m.png

链接队列:

单链表双链表单循环链表双循环链表:其中选择最简单的单链表 不带头结点的单链表:由于插入是在固定位置插入; 队列的抽象类来继承;

插入操作首先判断当前结点是否为空

删除操作首先判断操作是否合法;也就是删除的队列是否为空; 由于队列中定义了两个指针,所以要考虑另一个指针是否可以清空;所以当front == rear也就是只剩一个元素时,先把front 删除,然后判断是否满足该情况如果现在队列元素为空那么rear队尾指针也需要清空防止内存泄漏;

ydWeCUj2B6gKOG2KC_gjt.png

出队和入队操作都需要考虑当队列为空的情况,因为两个指针都需要初始化;

列车车厢重排:

xHDv9Fg12NbI-OU2t4Svs.png

//如何让for循环结束while循环也结束
bool flag = true;
while(flag){
    flag = false;
    for(){flag = true;}
}
    flag = 0;
    while(!flag){
        flag ^= 1;}
        实现两个一循环,
        flag %2
        取指针元素之前一定要先判断指针元素是否0
  • 依次取入轨的队头元素,直到队列为空

  • 进入一条最合适的缓冲轨道

  • 检查每条缓冲轨道的队头元素,将可以放入出轨的元素出队,进入出轨

  • 其实可以在进入缓冲轨道之前,先判断当前元素是不是可以直接出轨

    最优车厢时,也就是如果;如果选择进入非最大的轨道,那么比当前元素小的车厢就不能进入当前轨道,更不能进入最大轨道,因此可能需要更多的轨道来容纳此车厢。而如果选择最大的轨道就可以让更多的比当前元素小的车厢进入其他非最大轨道。

官方:

对于尾车厢编号最大的那个轨道,能进入此轨道的车厢可以进入其余所有轨道,但能进入其余轨道的车厢不一定能进入此轨道。
排队系统模拟:

系统的基本概况: 很长时间的排队监测,短时间的监测可能不准确; 模拟可以简化为排队系统事件: 到达事件,离开事件;

系统的模拟: 时间驱动的模拟:模拟每一秒发生的事件,有点浪费资源,需要时刻监视; 事件按照发生时间进行模拟,时间驱动的模拟;

事件生成: 顾客等待时间 到达时间间隔和每个服务的时间都是随机的;全部服从一定概率分布-如何模拟一个概率分布

随机服从均匀分布的事件生成: 间隔时间t服从某个区间[a,b]的均匀分布; 随机数都是等概率的符合均匀分布:

rand()*(b-a+1)/(RAND_MAX + 1) + a;??

单服务台排队系统的模拟系统:

新的顾客到达时间触发服务台为之服务需要在前一事件的完成时间的基础上

1. 问题

设计一个只有一个服务台的排队系统,希望通过这个模拟器得到顾客的平均排队时间。

顾客到达的时间间隔服从[arrivaLow, arrivalHigh]的均匀分布

服务时间长度服从[serviceTimeLow, serviceTimeHigh]间的均匀分布

一共模拟customNum个顾客

要求统计顾客的平均排队时间

2. 事件存储:  根据到达早离开早的特点可以用FIFO存储到达事件。因此只要处理到达事件就可 并不需要关心每一个单位时间在干什么,而是关心每个服务的时间覆盖区间。因此重点在于处理到达事件。

3. 模拟过程

生成所有的顾客到达事件,按到达时间排成一个队列;
队列的value就是到达时将爱你

设置当前时间为0
    ·依次处理队列中的每个元素,直到队列为空
    //出队操作
    
    ·检查顾客的到达时间和当前时间,计算等待时间,记入累计值;
    //比较并找到当前顾客服务的时间
    ·生成顾客服务时间
    
    ·将当前时间拨到该顾客的离开时间
    //更新当前可用的时间
返回累计值除以顾客数的结果。

4. 伪代码

totalWaitTime = 0;
设置顾客开始到达的时间currentTime = 0;
for (i=0; i<customNum; ++i) {
      生成下一顾客到达的间隔时间;
      下一顾客的到达时间currentTime  += 下一顾客到达的间隔时间;
      将下一顾客的到达时间入队;
 }
 //获得每个顾客的到达时间
 
从时刻0开始模拟;
while (顾客队列非空){
    取队头顾客;
    if (到达时间 > 当前时间)
          直接将时钟拨到事件发生的时间;
    else  收集该顾客的等待时间;
    生成服务时间;
    将时钟拨到服务完成的时刻;}
返回 等待时间/顾客数;
字符串

使用顺序表也就是数组存储: 字符数组来存储字符串,由于数组大小只能固定

  • 字符串的特点:需要对整个表而不是表中元素进行操作

C语言中使用的是字符数组 char s[9]; char *s = new string; strcat(s1,s2)需要保证s1的长度大于s1+s2 strcpy(s1,s2)要保证s1的长度要大于s2 C++使用string类,使用了动态数组;可以自动扩展数组的空间; s1=s1;s1+s2 数组类的插入删除比较困难,时间性能很差。遍历等很方便。 操作

  • 求字符串中字符的个数length(s)
  • 字符串输出disp(s)
  • 判断两个字符串大小
  • 字符串赋值copy(s1, s2)
  • 字符串连接cat(s1, s2)
  • 取子串substr(s, start, len)
  • 字符串插入insert(s1, start, s2)
  • 删除子串remove(s, start, len)

使用链接存储: 最简单的单链表,存储数据元素并存储next指针;常用计算机地址占用4个字节,不同计算机不同;因此会带来空间上的额外开销; 使用块状链表: 每个结点存放多个连续的字符,多个字符共用一个指针;由于插入和删除会引起数据的大量移动; 改进方法:每个结点不放满;允许有一定的空闲空间 那么每个结点就可以分割然后使用新的结点取保存,最后检查是否有结点可以合并来节约空间。

  • 删除 p3RIFoWQyKV0khE0GnWil.png

fxaX5rTRA20G-C1G0TC--.png

WHVOXcg7MyOAF4hTqdGMX.png 字符结构固定也就是说每个结点的字符个数是固定的不能增加超出限制,(而且还规定要有空余的空间); 块状链表的元素结构固定,因此字符A不能与下一个块的字符串进行归并,但是删除JKM之后字符I与字符NO可以进行归并。

  • 插入: k4RGCijGGWB4wuqDQ_Xfz.png

块状链表的元素结构固定,因此字符串UVWX分裂成UVWX,其中X与字符串C进行归并;

ZKxigWkK6cGBDL0qogNAJ.png

-hA9aawnCu_2TBKNSZzxd.png

树的定义:

每一个元素都只有一个前驱,可以有很多后继结点

  • 没有前驱的结点为根结点,没有后继结点的为叶结点

  • 内部结点

  • 每个结点后继结点的个数为结点的度,所有结点的最大度数为树的度

  • 结点的直接后继称为结点的子结点。结点的直接前驱称为结点的父结点。在树中,每个结点都存在着唯一的一条到根结点的路径

    • 兄弟结点
    • 祖先结点
    • 子孙结点
  • 结点多处的层次,从根结点开始为1,子孙结点依次加1,所有结点的最大层次为树的高度

  • 如果儿子结点之间有顺序那么有序树; 在有序树中,最左边的子树称为第一棵子树,最右边的子树称为最后一棵子树。

  • 如果儿子结点之间没有顺序那么无序树

  • 树的集合叫做森林;M棵互不相交的树的集合称为森林。显然,删去了一棵树的根,其子树的集合就形成了一片森林。

树的操作:

树的运算

  1. 建树create():创建一棵空树
  2. 清空clear():删除树中的所有结点
  3. 判空IsEmpty():判别是否为空树
  4. 求树的规模size():统计树上的结点数
  5. 找根结点root():找出树的根结点值;如果树是空树,则返回一个特殊值
  6. 找父结点parent(x):找出结点xx的父结点值;如果xx不存在或xx是根结点,则返回一个特殊值
  7. 找子结点child(x,i):找结点xx的第ii个子结点值; 如果xx不存在或xx的第ii个儿子不存在,则返回一个特殊值
  8. 剪枝remove(x,i):删除结点xx的第ii棵子树
  9. 遍历traverse():访问树上的每一个结点
树的抽象类

计算树的叶结点个数:

二叉树

最多只有两个子结点,需要分左右结点讨论

2.jpg

  • 特殊的两个树

满二叉树:  一棵高度为kk并具有2^k-12k-1个结点的二叉树称为满二叉树。也就是除了叶结点,所有其他结点都必须有两个结点。并且叶结点都处于最高层。 已知条件为该二叉树为满二叉树,则满足关系如下:n = 2^h-12m-1 = n, m = 2^(h-1)

完全二叉树:  在满二叉树的最底层自右至左依次(注意:不能跳过任何一个结点)去掉若干个结点得到的二叉树也被称之为完全二叉树。

  • 所有的叶结点都出现在最低的两层上。
  1. 对任一结点,如果其右子树的高度为kk,则其左子树的高度为kk或k+1k+1。

4.jpg

性质:

度数为0的节点数量为度数为2的结点数量+1,即叶子节点数量为度数为2的结点数量+1

归纳法可得: image.png

二叉树顺序存储

5.jpg

6.jpg

二叉树遍历

前序:根结点左结点右结点 中序:左结点根结点右结点 后序:左结点右结点根结点 层次遍历:按照层次排序遍历,下一层的遍历需要依靠上一层根结点遍历的顺序从左至右,将该曾的子结点遍历。、

前序遍历与中序遍历结果相同

  • 所有结点只有右子树的二叉树

前序遍历和后序遍历结果相同

  • 只有根结点的二叉树 相反:
  • 最多只有一个叶子结点

二叉树中序遍历结果序列的最后一个结点是叶子结点,则它一定是前序遍历结果序列的最后一个结点

前序+中序确定一棵二叉树

前序和后序不可唯一确定一个树,abba

二叉树的抽象类
  • 建树create():创建一棵空的二叉树
  • 清空clear():删除二叉树中的所有结点
  • 判空IsEmpty():判别二叉树是否为空树
  • 求树的规模size():统计树上的结点数
  • 找根结点root():找出二叉树的根结点值;如果树是空树,则返回一个特殊值
  • 找父结点parent(x):找出结点x的父结点值;如果x不存在或x是根,则返回一个特殊值
  • 找左孩子lchild(x):找结点x的左孩子结点值;如果x不存在或x的左儿子不存在,则返回一个特殊值
  • 找右孩子rchild(x):找结点x的右孩子结点值;如果x不存在或x的右儿子不存在,则返回一个特殊值
  • 删除左子树delLeft(x):删除结点x的左子树
  • 删除右子树delRight(x):删除结点x的右子树
  • 前序遍历preOrder():前序遍历二叉树上的每一个结点
  • 中序遍历midOrder():中序遍历二叉树上的每一个结点
  • 后序遍历postOrder():后序遍历二叉树上的每一个结点
  • 层次遍历levelOrder():层次遍历二叉树上的每个结点
二叉树的性质:

image.png

二叉树的顺序遍历:
二叉树的存储:

顺序表: 完全二叉树依靠数组的下标之间的关系进行确定 非完全二叉树不可直接使用:所以对缺失的元素也正常编号。浪费空间。 完全或者接近完全二叉比较好,由于数组空间有限,可能会浪费时间,所以尽量结点已知。

链表: 只存后继结点-二叉链表-单链表-只需要指向根结点的指针就可-二叉树标准存储方式-用的最多 后继前驱都有-三叉链表-双链表

二叉链表类设计

数据成员设计

由两个类组成:

  • 结点类:

    • 数据成员:数据及左右孩子的指针。
    • 结点的操作包括:构造和析构
  • 二叉树类: 数据成员:指向根结点的指针

结点类是树类的私有内嵌类

成员函数设计

  • 抽象类规定的所有函数

  • 二叉树是递归定义,所以操作可用递归实现

    • 递归函数必须有一个控制递归终止的参数
    • 每个需要递归实现的公有成员函数对应一个私有的、带递归参数的成员函数
    • 公有函数调用私有函数完成相应的功能
  • 有些操作需要先找到某个结点的地址

    • 增设一个私有成员函数find

find的实现

  • 如何找结点x:遍历,可以采用任一种遍历
  • 工作过程:采用前序遍历
if (根结点是x)  返回根结点地址
tmp = 左子树递归调用find函数
if (tmp不是空指针)  return tmp;
else return 对右子树递归调用find的结果

clear中引用传递,最后把结点制成了NULL。 因此删除左右子树时该结点已经被制成了NULL

CREATE和层次遍历想法一样 就是从上至下,同一代的儿子优先级一样。可以同时。 进队和出队构成了一个while循环。 createTree以BFS的方式输入一棵树

//DFS
template <class T>
void createTree(T flag)
{
    cout << "输入根结点:";
    cin >> x;
    if (x == flag)
        return;
    root = new Node(x);
    createTree(root, flag);
}
void createTree(Node *t, T flag)
{
    T ldata, rdata;
    if (t == NULL)
        return;
    cout << endl
         << "输入" << t->data << "的两个儿子(" << flag << "表示空结点):";
    cin >> ldata >> rdata;
    if (ldata != flag)
    {
        t->left = new Node(ldata);
        createTree(t->left, flag);
    }
    if (rdata != flag)
    {
        t->right = new Node(rdata);
        createTree(t->right, flag);
    }
}
友元函数

层次遍历每个结点并输出这个结点的值和当前结点的左右结点的值。

非递归实现:

栈或者队列非空的思想,一个结点处栈的同时其子结点进栈,也就是每次都当作一个根结点。

  • 前序
template <class Type>
void BinaryTree<Type>::preOrder() const { 
    linkStack<Node *> s; 
    Node *current; 
    cout << "前序遍历: "; 
    s.push( root ); 
    while ( !s.isEmpty() ) {
        current = s.pop(); 
        cout << current->data; 
        if ( current->right != NULL ) s.push( current->right ); 
        if ( current ->left != NULL ) s.push( current->left );
    } 
}

size()

template <class T>
int BinaryTree<T>::size() const
{
    linkStack<Node *> s;
    int count = 0;
    Node *current;

    if (root == NULL)
        return 0;
    s.push(root);
    while (!s.isEmpty())
    {
        current = s.pop();
        ++count;
        if (current->right != NULL) s.push(current->right);
        if (current->left != NULL)
            s.push(current->left);
    }
    return count;
}
  • 中序 记录树根第几次进栈; 首先分析(难点解决方案),然后画图实例化,最后代码并验证。
struct StNode { 
    Node *node;
    int TimesPop; 
    StNode ( Node *N = NULL ):node(N), TimesPop(0) { } 
};
template <class Type> void BinaryTree<Type>::midOrder() const { 
    linkStack<StNode> s; 
    StNode current(root); 
    cout << "中序遍历: "; 
    s.push(current); 
    while (!s.isEmpty()) { 
        current = s.pop(); 
        if ( ++current.TimesPop == 2 ) { 
            cout << current.node->data; 
            if ( current.node->right != NULL ) s.push(StNode(current.node->right )); 
            } else { s.push( current ); 
                if ( current.node->left != NULL ) s.push(StNode(current.node->left) );
            } 
    } 
}

一次

template <class Type>
void BinaryTree<Type>::midOrder() const
{
    linkStack<Node *> s;
    Node *tmp;
  
    cout << "中序遍历: ";
    s.push(root);
    for (tmp = root->left; tmp != NULL; tmp = tmp->left)
        s.push(tmp);

    while (!s.isEmpty()) {
        tmp = s.pop();
        cout << tmp->data ; 
        if (tmp ->right != NULL) {
            s.push((tmp->right);
        for ( tmp->right->left; tmp != NULL; tmp = tmp->left) 
            s.push(tmp);
    }
}
后序遍历:
template <class Type>
void BinaryTree<Type>::postOrder() const {

    linkStack< StNode > s; 
    StNode current(root); 
    cout << "后序遍历: "; 
    s.push(current); 
    while (!s.isEmpty()) { 
        current = s.pop(); 
        if ( ++current.TimesPop == 3 ) {
            cout << current.node->data; 
            continue; 
        } 
        s.push( current ); 
        if ( current.TimesPop == 1 ) { 
            if ( current.node->left != NULL ) s.push(StNode( current.node->left) );
            } else { 
            if ( current.node ->right != NULL ) s.push(StNode( current.node->right ) ); 
            } 
        }
}

以非递归方式统计树的高度。

template <class Type>
int BinaryTree<Type>::getHeight() const
{
    linkStack<StNode> s;
    StNode current(root);
    int last = 0;

    if (root == NULL)
        return 0;
    s.push(current);

    while (!s.isEmpty())
    {
        current = s.pop();
        if (++current.TimesPop == 3)
        {
            if (current.node->right != NULL && last + 1 > current.height)
                ++last;
            else
                last = current.height;
            continue;
        }
        if (current.TimesPop == 1)
        {
            s.push(current);
            if (current.node->left != NULL)
                s.push(StNode(current.node->left));
        }
        else
        {
            if (current.node->left != NULL)
                current.height += last;
            
                s.push(current);
            if (current.node->right != NULL)
                s.push(StNode(current.node->right));
        }
    }
    return last;
}

优先队列

本质 按优先级也就是按照优先级顺序:

  • 结构上是完全二叉树

最大化堆-根结点最大 最小化堆-根结点最小-也就是值越小优先级最高

  • 父子之间满足一定关系。

优先级队列本质完全二叉树使用顺序存储

用二叉堆实现优先级队列

  • 取队头元素的操作就是返回根结点的元素值

  • 出队操作就是删除根结点

  • 入队操作就是在最后一层的第一个空位置上添加一个元素,但添加后要调整元素的位置,以保持堆的有序

  • 方法二: 不考虑中间状态,保证所有元素加入后满足堆的特性

    • 做法: 利用堆的递归定义,将左子树和右子树调整成堆,对根结点调用向下过滤percolateDown,以恢复堆的有序性。
    • 复杂度: 最坏情况下是O(N)O(N),所以我们一般会使用这种方法
类设计
  • 存储 不需要存储指针只需要存储元素; 动态数组成员- :数组头指针,数组大小,以及当前排队队列的长度 存储完全二叉树所以是顺序结构;
// 递归方法等价于以逆向层次的次序对结点调用percolateDown
template <class Type> void priorityQueue<Type>::buildHeap() {
    for (int i = currentSize / 2; i > 0; i--) percolateDown( i );
} // 带初值的构造函数 
template <class Type> priorityQueue<Type>::priorityQueue(const Type *items, int size) : maxSize(size + 10 ), currentSize(size) { 
    array = new Type[maxSize]; 
    for(int i = 0; i < size; i++) 
        array[i + 1] = items[i]; 
    buildHeap(); 
}
  • 操作 public: 创建一个队列:create():initsize

  • 入队:enQueue(x):结构性(最底层的第一个空位)-完全二叉树,有序性(父结点小于子结点;(向上过滤)

  • 出队:deQueue()

  • 读队头:getHead

  • 判队空:isEmpty

  • 入队: enQueue(x):结构性(最底层的第一个空位)-完全二叉树,有序性(父结点小于子结点;(向上过滤) 复杂度:常量时间,最坏情况对数,平均使用概率模拟可以获取较好的结果。

  • 出队: 根结点出队,则交换根结点和最后一层的叶结点(叶奇点好删除)同时重塑新队列(将新的大的值向下过滤) 根结点永远下标为1 需要结合向下过滤函数左右儿子中最小的;

操作要求:

  • 保证结构性:将最后一个结点移到根结点
  • 保证有序性:向下移动到合适的位置(向下过滤

复杂度:  最坏和平均都是对数的时间复杂度,由于高度是对数的。平均也是对数时间

image.png

  • 建堆: 不考虑中间状态考虑最终状态;最后在排队 递归最小状态是23,对根结点1使用percolatedown 。 下标从1开始

  • 时间复杂度 最坏时间:N-h 最大的交换个数;O(N) 定理:  对于一棵高度为hh,包含了N=2h1N = 2^h - 1个结点的满二叉树,向下过滤中交换次数和的最大值为NhN – h。 交换次数和比较次数;

二项堆:

  1. 左堆:左子树比右子树高 斜堆:先放右交换左右子树
  2. 二项堆,表示方法:  堆的结构是唯一的,正好对应二进制表示

二项堆的操作:

  • 归并操作: 由低到高依次归并两个优先级队列中高度相同的树——将根结点大的作为根结点小的树的子树。如果由于前一次归并而出现三棵高度相同的树时,留下一棵,归并其余两棵。最坏时间复杂度是O(logN)O(logN)。 -出队操作,可能会使根结点下的子树分散开,因此需要我们重新将其整合。

  • 最坏时间复杂度是O(logN)O(logN)。

  • 插入结点,也就是插入一个单结点的树,最坏时间复杂度是O(logN)O(logN)。平均是2次操作即可。

多服务台排队系统:

与单服务台系统,不同的是到达事件和离开事件不再一致,可以使用计数pop出队的次数 或者使用优先级队列,和先来先服务的队列。同时存储到达和离开 收集服务台总的工作时间可以出现在 处理到达事件时统计当前客户的服务时间;处理离开事件时处理当前在等待的第一位客户的服务时间。

  • STL中的优先级队列

    • 头文件:<queue>
    • 类模板:priority_queue
  • 存储设计:  用优先级队列存放所有事件,优先级是事件发生的时间。

  • 设计要求:  模拟n个顾客的平均等待时间

    • 成员函数:

      • 构造函数:接收模拟参数
      • avgWaitTime:执行模拟并最终给出平均等待时间
    • 数据成员:  模拟参数(假设到达和服务时间都服从均匀分布)

    随机数初始化srand(time(NULL))

哈希表

- 原数组存储值,与该数组的下标相同创建一个数组用来存储原数组的状态;
- 由于数组的长度依靠数据的范围;
也就是定义域过大,但是实际上使用存储状态的数组元素数量很小。
  • 优化: 散列函数(hash function)很大的自变量映射到小值域;函数值作为该数据存储的下标。
    • 可能存在冲突(因此哈希不可逆),也就是值域返回到自变量(能转换成整数)
    • 哈系表的设计

哈希函数:

函数值对应元素的存储位置,关键字作为元素的输入; 而且与该表的存储的数组长度有关,需要为0~n-1;

  • 优化:计算速度快,减少冲突;

  • 直接地址法:类似于线性函数(k斜率,b截据) 可以缩放,平移变换;

  • 除留余数法:类似于循环队列;由于除数的选择不够恰当可能导致数组分布不均匀或者浪费;保证在数组下标内

  • 数字分析法: 找到区分度大的自变量,也就是把关键字数据位数缩短。

  • 平方取中法: 取值域中能够反映数据多样的量分析;

    • 适用于关键字中各位的分布都比较均匀,但关键字的值域比数组规模大 将关键字平方后,取其结果的中间各位作为散列函数值 由于中间各位和每一位数字都有关系,因此均匀分布的可能性较大
  • 折叠法:将数据元素安某个长度分开然后相加,减少了自变量也就是数据的位数;

哈希冲突:

  • 闭散列表(圈圈) 线性探测法:使用线性步长;每次探测 二次探测法:使用二次函数的步长; 再次散列法:使用第几次探测和一个新的哈希函数;

  • 开散列表(数组和链表相结合,也就是数组值重合时,在数组值处添加链表)

线性探测法:

查找 继续对输入的键值进行哈希函数;遇到冲突也就是元素不相同时继续进行探测查找元素;

  • 判断元素是否存在也就是先进行哈希函数找到对因的地址,如果地址有元素,判断是否为当前值,从该位置开始查找一直到元素为空,则说明没找到; 也可能出现整个数组都是满员的情况

    计算 addr = H(key) 从 addr 开始遍历数组 if (找到) 返回 true if (内容为空) 返回 false ++addr 返回 false

删除 先查找,然后删除,采用迟删除法,也就是对该位置做了一个标记;

image.png

线性探测法类的缺陷:

  • 没有保证表长为素数

    • 在构造函数中增加素数检测。如果实际参数的表长不是素数,则将大于等于表长的最小素数作为表长。 需要对函数参数进行优化
  • 运行一段时间以后,所有数组元素都成为active或deleted,所有操作的时间性能都是O(N)

    • 可以增加一个整理散列表的功能,将被删元素真正删去
    • 可以增加一个数据成员记录deleted的单元数,自动调用整理函数
二次探测法:
  • 会不会进入死循环概率问题也就是每次都进入一个元素

  • 或者避免某个元素,某些元素访问不到

    如果表的大小是一个素数,表至少有一半是空的,新的元素总能被插入。

    • 定理:如果采用二次探测法,并且表的大小是一个素数,那么,如果表至少有一半是空的,新的元素总能被插入。而且,在插入过程中,没有一个单元被探测两次。

-或者平方的原因使得遍历需要计算的数字特别大;

只需要一次移位、一次加法、两次减法 可以在前一个遍历数据的基础上,使用平方差公式得到此次需要遍历需要的步数。由于i距离M很小,所以此次2* i-1距离M非常小也就是可以使用减法代替MOD取模操作;

开散哈系表:

 使用数组和链表相结合的方式,也就是数组的哈希值重合时,选择在哈希值处建立一个链表,表示重复元素;

图:

图的标准存储方式:

邻接表,也就是所有元素生成的数组和由该数组出发的结点形成的链表;

邻接矩阵比较简单,也就是生成N* N大小的数组,存放所有的出度入度和边;

深度优先搜索:

类似二叉树的先序遍历,对于一个根结点,先访问该根结点,然后访问根结点的后继结点,如果某一个后继结点访问完毕,那么遍历根结点其他的后继结点直到所有后继结点都访问完毕,该根结点访问完毕,于是回到该根结点的根结点。

算法的复杂度在于:使用哪种数据存储方式存储图,如果是邻接矩阵,那就是N^2如果是邻接表那么就是E+V

广度优先搜索:

类似于层次遍历: 将最小的顶点放入队列,使用队列就可以自己做完任务啦。由于不是单方向的所以需要给遍历以后的元素作标记。 同时由于从某一个顶点出发的某一次遍历不能完全覆盖图中的所有结点,所以需要一次遍历结束以后检查其他所有元素是否访问完毕。 或者使用顺序遍历,其中一次遍历过程中就可以改变数组后序元素是否遍历过的标记的值。

当节点数为9时,图的结构如下图。此时可以验证如下节点遍历序列既能为深度优先搜索序列,也能为广度优先搜索序列:8->4->9->2->5->1->3->6->78−>4−>9−>2−>5−>1−>3−>6−>7

算法 复杂度和广度优先搜索一致;

最后得到的结果就是一个由V个结点组成的一条简单路径,其含有V-1条边,整个图中所有的其他边都是无效的,个数为E-(V-1); 如果是无向图,那么每条边都会被无效访问两次;

如果结果不只有一个生成树,是一片森林的话,一个树就需要加上1

欧拉回路

可以把所有的位置和链接抽象为一个图; 幸运星” 图论的知识:

  • 所有结点的度均为偶数,应该存在欧拉回路,未得到证明?

我们可以给每一个结点多一个机会,遍历更多的点,并记录下来,最后遍历所有的结点

欧拉回路存在的条件:

  • 必须是无向连通图。 D的证明:若G为无向图,则图G′的各顶点的度为偶数;若G为有向图,则图 G′的各顶点的入度等于出度。
  • 检查每个结点的度数是否为偶数