数据结构-学习笔记

绪论

什么是数据结构

数据结构的研究对象

  • 研究一组有特定关系的数据的存储处理
  • 通过抽象的方法

数据结构的研究内容

  • 数据之间的逻辑关系:存储实现(如何存储某种逻辑关系)
    • 集合结构:数据元素放在一起,但是元素间没有关系
    • 线性结构:数据元素的有序序列,每个元素有一个前趋和一个后继
    • 树形结构:层次关系的数据,除了根元素,每个元素有且只有一个前趋,后继数目不限
    • 图形结构:元素之间相互关联,每个元素可以有多个前趋和后继

在这里插入图片描述

  • 关系对应的操作:运算实现(在这种存储模式下,如何实现相关操作)
    • 创建:创建空的数据结构
    • 清除:清空数据结构
    • 插入:在指定位置插入新元素
    • 删除:删除某个元素
    • 搜索:搜索满足特定条件的元素
    • 更新:修改某个元素的值
    • 访问:访问数据结构中的某个元素
    • 遍历:按照某种次序,访问每个元素有且只有一次

数据结构的实现

数据结构的存储实现

  • 需要储存的信息
    • 一组数据元素
    • 数据元素之间的关系
  • 物理结构
    • 存储结点:一个简单变量、结构体变量或者对象
    • 存储数据元素之间的关系:用结点间的关系来表达
    • 存储附加信息:便于运算的“哑结点”
  • 如何存储元素及其关系
    • 顺序存储:用存储的位置表示元素之间的关系,主要用数组实现
    • 链接存储:用指针显式指出元素之间的关系,如链表
    • 哈希存储:主要用于表示集合这种元素间没有关系的结构,方便查找
    • 索引存储:分为数据区和索引区,在索引区存放关系
      数据结构的运算实现
  • 操作怎么实现
    • 每个运算对应一个算法
    • 每个算法用一个函数表示
    • 每个数据结构有一组函数表示其对应的操作

算法与数据结构

算法优化指的是优化算法的时间性能和空间性能。需要慢慢分析,逐步优化。

以最大连续子序列和问题为例来看算法优化问题。

给定整数序列,寻找最大的子序列和,例如,对于序列{-2, 11, -4, 13, -5, 2},答案是20。

  1. O ( N 3 ) O(N^3) O(N3)算法:枚举法:
    这是最直观的算法。用起点和终点来确认一个子序列,这样我们就可以用两层的嵌套循环,枚举出所有的子序列:
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. O ( N 2 ) O(N^2) O(N2)算法:枚举子序列
    在枚举的时候, i i i j + 1 j+1 j+1 这个子序列的和,没必要再用一个for循环,可以直接在 i i i j j j 这个子序列的和上再加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. O ( N l o g N ) O(NlogN) O(NlogN)算法:分治法
    分成不同的情况来解决:
  • 情况1:最大和子序列位于前半部,可递归计算
  • 情况2:最大和子序列位于后半部,可递归计算
  • 情况3:最大和子序列从前半部开始但在后半部结束
    • 从两半部分的边界开始
    • 通过从右到左的扫描来找到左半段的最长序列
    • 从左到右的扫描找到右半段的最长序列
    • 把这两个子序列组合起来,形成跨越分割边界的最大连续子序列
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;
    else
        if (maxRight > maxLeftTmp + maxRightTmp)  {
            start = startR;
            end = endR;
            return maxRight;
        }
        else return maxLeftTmp + maxRightTmp;
}
  1. O ( N ) O(N) O(N)算法:在枚举法的基础上改进
  • 现象:和为负的子序列不可能是最大连续子序列的开始部分
  • 结论:当检测出一个负的子序列时,可以让start直接增加到 j + 1 j+1 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;
}
最大连续子序列
给定一个整数序列,在其中找出连续的一段,使得这段连续子序列的和最大。若该整数序列中所有数全部为负数,则答案为零。

输入描述:
第一行一个整数n (1 ≤ n ≤ 2000),表示给定整数序列的长度。
第二行n个整数,用空格间隔,表示该序列。

输出描述:
一行一个整数,表示最大连续子序列的和。

示例 1:
输入:
5
2 -1 3 -4 3
输出:
4
#include <iostream>

#define N 2010

using namespace std;

int a[N], sum[N];

int main() {
    int n, ans = 0;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < i; j++)
            ans = max(sum[i] - sum[j], ans);
    cout << ans << endl;
    return 0;
}

线性表

线性表的定义

线性表的概念

  • 线性表是处理线性结构的数据结构。

  • 线性结构的数据指的是:

    • 除了 A 0 A_0 A0 A n − 1 A_{n-1} An1外,每个元素都有唯一的前趋和后继
    • 对于每个 A i A_i Ai,它的前驱是 A i − 1 A_{i-1} Ai1,它的后继是 A i + 1 A_{i+1} Ai+1
    • A 0 A_0 A0只有后继没有前驱, A n − 1 A_{n-1} An1只有前驱没有后继。

线性表的基本操作

  • 创建空的线性表
  • 清除所有元素
  • 求线性表的长度
  • 在第i个位置插入一个元素
  • 删除第i个位置的元素
  • 搜索某个元素在线性表中是否出现
  • 访问线性表的第i个元素
  • 遍历线性表

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

  • 和线性表的操作相比,少了创建操作,因为这可以用每个类的构造函数来完成。
  • 多了虚析构函数,是为了防止内存泄漏,把申请的动态内存析构掉。
template <class elemType>
class list {
  public: 
     // 清除
     virtual void clear() = 0;
     // 长度
     virtual int length() const = 0;
     // 插入
     virtual void insert(int i, const elemType &x) = 0; 
     // 删除
     virtual void remove(int i) = 0;
     // 搜索
     virtual int search(const elemType &x) const = 0;
     // 访问
     virtual elemType visit(int i) const = 0;
     // 遍历
     virtual void traverse() const = 0;
     // 析构
     virtual ~list() {};
};

线性表的抽象类中为什么一定要定义一个虚析构函数?
解释:
虚析构函数是为了解决父类指针指向子类对象时,释放子类对象的资源时,释放不完全,造成的内存泄漏问题。

顺序表

顺序表的概念和设计
顺序表指的是线性表的顺序存储,也就是说线性表中结点存放在存储器上一块连续的空间中,即一个数组。
在这里插入图片描述

  • 数据成员
    • 表元素类型的指针 data
    • 数组规模 maxSize
    • 数组中的元素个数 currentLength

顺序表类定义

template <class elemType>
class seqList: public list<elemType> { 
private:
    elemType *data;
    int currentLength;
    int maxSize;
    // 为了解决插入元素时,数组容量不够的问题
    void doubleSpace();
public:
    seqList( int initSize = 10 );
    // 析构函数:还掉动态空间
    ~seqList( )  {  delete [] data;  }
    // 清空数组元素:只要把表长设为0
    void clear( )  {   currentLength = 0;   }
    // 数组长度:只需要返回这个私有成员变量currentLength
    int length( ) const  {   return currentLength;   }
    void insert( int i, const elemType &x); 
    void remove( int i );  
    int search( const elemType &x ) const ;
    // 访问数组元素:返回下标为i的数组元素
    elemType visit( int i) const { return data[i]  ; }
    void traverse( ) const ;
};

顺序表成员函数的实现

  • 构造函数:申请一个空表
template <class elemType>
seqList<elemType>::seqList(int initSize) {
    data = new elemType[initSize]; 
    maxSize = initSize; 
    currentLength = 0;
} 
  • 搜索函数:用for循环进行查找
template <class elemType>
int seqList<elemType>::search (const elemType &x) const {
    int i;
    // 空循环,不满足循环条件时跳出
    for (i = 0; i < currentLength && data[i] != x; ++i) ;
    if (i == currentLength)
        return -1; 
    else return i;
}
  • 遍历函数
template <class elemType>
void seqList<elemType>::traverse() const {
    cout << endl;
    for (int i = 0; i < currentLength; ++i) 
        cout << data[i] << ' ';
}
  • 插入函数
template <class elemType>
void seqList<elemType>::insert(int i, const elemType &x) {
    // 如果数组放满了,先扩大数组空间
    if (currentLength == maxSize) 
        doubleSpace(); 
    // 从后往前,元素后移
    for ( int j = currentLength; j > i; j--) 
        data[j] = data[j-1];
    // 空出空间,插入元素,表长加1
    data[i] = x;
    ++currentLength;
}
  • doubleSpace函数:扩大数组空间
template <class elemType>
void seqList<elemType>::doubleSpace() { 
    // 保存指向原来空间的指针
    elemType *tmp = data;
    // 重新申请空间
    maxSize *= 2;
    data = new elemType[maxSize];
    // 拷贝原有数据
    for (int i = 0; i < currentLength; ++i) 
        data[i] = tmp[i];
    // 清除原来申请的空间
    delete [] tmp;
} 
  • 删除函数:
template <class elemType>
void seqList<elemType>::remove(int i) {
    // 后面所有元素前移,表长减1
    for (int j = i; j < currentLength - 1; j++) 
        data[j] = data[j+1] ;
    --currentLength;
} 

总结

  • 由于要保持逻辑次序和物理次序的一致性,顺序表在插入删除时需要移动大量的数据,性能不太理想;
  • 由于逻辑次序和物理次序的一致性使得定位访问的性能很好
  • 顺序表比较适合静态的、经常做线性访问的线性表。
序列操作1
现有一个空的整数序列,需要你对其进行如下操作:
1.若命令为1 x,表示需要在这个序列末尾新插入整数x。
2.若命令为2 x,表示需要删除从前向后第x个数字,数据保证x一定小于等于当前序列中整数的个数。
3.若命令为3 x,表示需要输出从前向后第x个数字,数据保证x一定小于等于当前序列中整数的个数。

输入描述:
第一行一个整数m,表示操作的次数。数据保证m ≤ 10^3。
接下来m行,每行两个整数,第一个数字表示该次操作的类型,第二个数字x表示该此操作的参数,具体如题目所表述。

输出描述:
一行,若干个整数,为命令3输出的数字,用空格隔开。

示例 1:
输入:
6
1 1
1 2
1 3
2 2
3 1
3 2
输出:
1 3
#include <iostream>

using namespace std; 

int a[1010], cnt;

int main() {
    int m, t, x;
    cin >> m;
    while (m--) {
        cin >> t >> x;
        if (t == 1)
            a[++cnt] = x;
        else if (t == 2) {
            for (int i = x; i < cnt; i++)
                a[i] = a[i + 1];
            cnt--;
        }
        else
            cout << a[x] << " ";
    }
    return 0;
}

单链表

单链表的概念和设计
在这里插入图片描述

  • 单链表的概念:在单链表中,每个结点由数据元素和指针构成。该指针指向它的直接后继结点,最后一个结点的后继指针为空,即没有后继节点。
  • 单链表类的设计:
    • 存储设计:需要定义一个结点类,单链表类的数据成员包括头指针和链表长度currentLength
    • 工具函数设计:
      • 线性表的抽象函数都需要实现;
      • 链表的插入、删除操作都需要将指针移到被操作结点的前一结点。通过设计函数move实现找到某一个结点的位置的功能。

单链表类定义

template <class elemType>
class sLinkList: public list<elemType> { 
    private:
        // 定义一个结点的结构体,里面的所有成员都是公有的
        struct node {                                  
            elemType data;
            node *next;
            node(const elemType &x, node *n = NULL) { data = x; next = n; }
            node( ):next(NULL) { }
            ~node() {};
        };
 
        node  *head;                                         
        int currentLength;                                  
        node *move(int i) const;                  

    public:
        // 构造函数:创建空的单链表
        sLinkList() { 
            head = new node;
            currentLength = 0;
        }
        // 析构函数:调用clear把单链表的所有结点都还掉,再把头结点还掉
        ~sLinkList() { clear(); delete head; } 
        void clear() ;
        // 表的长度:返回私有的成员变量
        int length() const { return currentLength; }
        void insert(int i, const elemType &x); 
        void remove(int i);  
        int search(const elemType &x) const  ;
        elemType visit(int i) const;
        void traverse() const ;  
}; 

单链表节点中指针的作用是什么?为什么顺序表不需要指针?
解释:
单链表节点中指针的作用是指向当前节点的后驱节点,如果指针为空说明无后驱。 顺序表中存储数据的物理地址是连续的,故无需指针便可查找到后驱节点。

在单链表中,头指针的作用?
解释:
以确定线性表中第一个元素对应的存储位置。
单链表成员函数实现

  • clear():把单链表变成一个空表。注意需要把所有结点的空间还给系统。
template <class elemType>
void sLinkList<elemType>::clear() { 
    node *p = head->next, *q;
    
    head->next = NULL;
    // 只要p不是空的,把p的下一个结点记下来,删掉p,再把q赋给p
    while (p != NULL) {     
        q = p->next;
        delete p;
        p=q;
    }
    currentLength = 0;
} 
  • insert(i,x):在第i个位置插入元素x。先让指针指向第i-1个元素,之后执行插入过程,即申请一个存放x的结点,让该结点的后继指针指向结点i,让第i-1个结点的后继指针指向这个新结点。
template <class elemType>
void sLinkList<elemType>::insert(int i, const elemType &x) {
    node *pos;
    
    // 通过move函数找到第i-1个元素的地址
    pos = move(i - 1);
    pos->next = new node(x, pos->next);
    ++currentLength;
}
  • move(i): 返回指向第i个元素的指针。
template <class elemType>
sLinkList<elemType>::node * sLinkList<elemType>::move(int i) const {
    node *p = head;  
    while (i-- >= 0) p = p->next;
    return p;
}
  • remove(i):删除第i个位置的元素。先找到第i-1个结点,让该结点的后继指针指向第i+1个结点,释放被删结点空间。
template <class elemType>
void sLinkList<elemType>::remove(int i) {
    node *pos, *delp;
    
    // 通过move函数找到第i-1个元素的地址
    pos = move(i - 1);
    // 找到被删结点的位置,让第i-1个元素的后继指针指向第i+1个结点
    delp = pos->next;
    pos->next = delp->next;
    // 释放被删结点的空间
    delete delp;
    --currentLength;
}
  • search(x):搜索某个元素在线性表中是否出现。从头指针的后继结点开始往后检查链表的结点直到找到x或查找到表尾。
template <class elemType>
int sLinkList<elemType>::search(const elemType &x) const {
    // 指针指向第一个元素A_0
    node *p = head->next;
    
    int i = 0;
    while (p != NULL && p->data != x) {
        p = p->next; 
        ++i;
    }
    // p为NULL表示找到表尾都没有找到
    if (p == NULL)
        return -1;
    else 
        return i;
}
  • visit(i):访问线性表的第i个元素。通过move(i)找到第i个结点,返回该结点的数据部分
template <class elemType>
elemType sLinkList<elemType>::visit(int i) const {
    return move(i)->data;
}
  • traverse():遍历运算。从头结点的直接后继开始重复:输出当前结点值,将后继结点设为当前结点,直到当前节点为空。
template <class elemType>
void sLinkList<elemType>::traverse() const {
    node *p = head->next;
    cout << endl;
    while (p != NULL) {
        cout << p->data << "  ";
        p=p->next;
    }
    cout << endl;
}
序列操作2
现有若干个空的整数序列,需要你对其进行如下操作:

若命令为1 x y,表示需要在第x个序列末尾新插入整数y。
若命令为2 x,表示需要在第x个序列末尾删除一个数字,删除前保证该序列不为空。
若命令为3,表示需要查询当前所有序列中的所包含的元素,每个序列单独一行,对于每个序列按照输入的顺序输出。
数据保证命令3总数不超过2条。

输入描述:
第一行两个整数n m,表示序列的的数量和操作的次数。数据保证n ≤ 10^5 m ≤ 5 * 10^5。
接下来m行,每行表示一条命令,具体如题目所表述。

输出描述:
每次执行命令3输出n行,每行包含若干整数并用空格隔开,若该序列为空,则此行输出none。

示例 1:
输入:
2 7
1 1 3
3
1 1 4
1 2 1
1 2 2
2 1
3
输出:
3
none
3
1 2

#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;
}

双链表

双链表的概念和设计

  • 双链表概念:在连接实现中,如果每个结点既保存直接后继结点的地址,也保存直接前驱结点的地址,则该链表被称为双链表。
    在这里插入图片描述
  • 双链表类的设计:
    • 链表类的数据成员:头指针、尾指针,链表长度currentLength
    • 必须定义一个结点类。
    • 链表类必须实现线性表的所有操作,故链表类从线性表的抽象类继承。
    • 链表的插入、删除操作都需要将指针移到被操作结点的前一结点。通过设计函数move实现。

双链表的类定义

template <class elemType>
class dLinkList: public list<elemType> {
    private:
        struct node {                                         
            elemType  data;
            node *prev, *next; 
            node(const elemType &x, node *p = NULL, node *n = NULL) { data = x; next = n; prev = p; }
            node( ):next(NULL), prev(NULL) {}
            ~node() {}
        };

        node *head, *tail;                 
        int currentLength;  
        node *move(int i) const;
        
    public:
        dLinkList();
        // 析构函数:先调用clear删除所有元素,再删除头尾结点
        ~dLinkList() {
            clear(); 
            delete head; 
            delete tail;
        }

        void clear();
        // 返回元素数量
        int length() const { return currentLength; }
        void insert(int i, const elemType &x); 
        void remove(int i);  
        int search(const elemType &x) const;
        elemType visit(int i) const;
        void traverse() const;  
};

双链表成员函数实现

  • 构造函数:创建一个双链表就是创建一个只有头尾结点的链表,其中头结点的前驱为空,尾结点的后继为空。
template <class elemType>
dLinkList<elemType>::dLinkList() { 
    head = new node;
    head->next = tail = new node;
    tail->prev = head;
    currentLength = 0;
}
  • 插入:让指针指向第i个元素,执行插入过程,具体方式基本与单链表一致。
template <class elemType>
void dLinkList<elemType>::insert(int i, const elemType &x) {
    node *pos, *tmp;

    pos = move(i);                  
    tmp = new node(x, pos->prev, pos);
    pos->prev->next = tmp;        
    pos->prev = tmp;            

    ++currentLength;
}
  • 删除:让指针指向第i个元素,执行删除过程,具体方式基本与单链表一致。
template <class elemType>
void dLinkList<elemType>::remove(int i) {
    node *pos;

    pos = move(i);                       
    pos->prev->next = pos->next;           
    pos->next->prev = pos->prev

    delete pos;
    --currentLength;
}
  • 双链表其他操作和单链表基本相同。

思考双链表相比于单链表的优势,考虑在长为n的单、双链表中,知道指针p指向某结点,那么删除该结点的复杂度分别是 O ( N ) , O ( 1 ) O(N),O(1) O(N),O(1)

队列安排
一个学校里老师要将班上N个同学排成一列,同学被编号为1∼N,他采取如下的方法:

先将1号同学安排进队列,这时队列中只有他一个人;
2−N号同学依次入列,编号为i的同学入列方式为:老师指定编号为i的同学站在编号为1 ~ (i-1)中某位同学(即之前已经入列的同学)的左边或右边;
从队列中去掉M (M<N)个同学,其他同学位置顺序不变。
在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。

输入描述:
第1行为一个正整数N,表示了有N个同学。
第2−N行,第i行包含两个整数k p,其中k为小于i的正整数,p为0或者1。若p为0,则表示将i号同学插入到k号同学的左边,p为1则表示插入到右边。
第N+1行为一个正整数M,表示去掉的同学数目。
接下来M行,每行一个正整数x,表示将x号同学从队列中移去,如果x号同学已经不在队列中则忽略这一条指令。

输出描述:
一行,包含最多N个空格隔开的正整数,表示了队列从左到右所有同学的编号,行末换行且无空格。

示例 1:
输入:
4
1 0
2 1
1 0
2
3
3
输出:
2 4 1
#include <cstdio> 
#include <cstring> 
#include <iostream> 
#include <algorithm> 
#define N 100005 
using namespace std; 

struct node { 
    int l, r; 
}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); 
    return 0; 
}

栈的定义

栈的概念

  • 栈是线性表的一种;
  • 允许进行插入和删除操作的一端被称为栈顶,相对地,把另一端称为栈底;
  • 这是一种后进先出(LIFO, Last In First Out)或先进后出(FILO, First In Last Out)结构,最先(晚)到达栈的结点将最晚(先)被删除。

在这里插入图片描述

栈的基本操作

  • 创建一个栈create():创建一个空的栈;
  • 进栈push(x):将x插入栈中,使之成为栈顶元素;
  • 出栈pop()删除栈顶元素并返回栈顶元素值;
  • 读栈顶元素top():返回栈顶元素值但不删除栈顶元素;
  • 判栈空isEmpty():若栈为空,返回true,否则返回false

栈的抽象类

template <class elemType>
class stack {
    public: 
        virtual bool isEmpty() const = 0; 
        virtual void push(const elemType &x) = 0; 
        virtual elemType pop() = 0;              
        virtual elemType top() const = 0;
        // 虚析构函数防止内存泄漏
        virtual ~stack() {}
}; 

顺序栈

顺序栈的概念
顺序栈指栈的顺序实现。

  • 用连续的空间存储栈中的结点,即数组。
  • 下标为0的一端为栈底。
  • 进栈和出栈总是在栈顶一端进行,不会引起类似顺序表中的大量数据的移动。

顺序栈类定义

template <class elemType>
class seqStack: public stack<elemType>{
    private:
        elemType *elem;
        int top_p;
        int maxSize;
        void doubleSpace();
    public:
        seqStack(int initSize = 10)// 析构函数
        ~seqStack() { delete [] elem; }
        // 判断栈是否为空,若top_p的值为-1,返回true,否则返回false。
        bool isEmpty() const { return top_p == -1; }      
        void push(const elemType &x)// 出栈:返回栈顶元素,并把元素数量减1
        elemType pop() { return elem[top_p--]; }
        // top:与pop类似,只是不需要将top_p减1
        elemType top() const { return elem[top_p]; }
}

顺序栈成员函数实现

  • 构造函数:按照用户估计的栈的规模申请一个动态数组。
template <class elemType>
seqStack<elemType>::seqStack(int initSize) {
    elem = new elemType[initSize];
    maxSize = initSize ;
    top_p = -1;  
}
  • push(x):将top_p加1,将x放入top_p指出的位置中。但要注意数组满的情况。
template <class elemType>
void seqStack<elemType>::push(const elemType &x) { 
    if (top_p == maxSize - 1)
        doubleSpace();
    elem[++top_p] = x; 
}

顺序栈性能分析

  • 除了进栈操作以外,所有运算实现的时间复杂度都是O(1)。
  • 进栈运算在最坏情况下的时间复杂度是O(N)。(需要doubleSpace)
  • 均摊分析法:最坏情况在N次进栈操作中至多出现一次。如果把扩展数组规模所需的时间均摊到每个插入操作,每个插入只多了一个拷贝操作,因此从平均的意义上讲,插入运算还是常量的时间复杂度
验证栈序列
给出两个序列 pushed 和 poped 两个序列,其取值从 1 到n (1 ≤ n ≤ 10^5)。
已知入栈序列是1 2 3 ... n,如果出栈序列有可能是 poped,则输出Yes,否则输出No。每个测试点有多组数据。

输入描述:
第一行一个整数q,询问次数。
接下来q个询问,对于每个询问:
第一行一个整数n表示序列长度;
第二行n个整数表示出栈序列。

输出描述:
对于每个询问输出单独一行答案。如可能,则输出Yes;反之,则输出No。

示例 1:
输入:
2
5
5 4 3 2 1
4
2 4 1 3
输出:
Yes
No
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 100005 
using namespace std;

int a[N], b[N], s[N], top, n;

int main() {
    int T;
    scanf("%d", &T);
    for (int i = 0; i <100000; i++)
        a[i] = i + 1;
    while (T--) {
        memset(b, 0, sizeof(b));
        memset(s, 0, sizeof(s));
        scanf("%d", &n);
        for (int i = 0; i < n; ++i) scanf("%d", &b[i]);
        int j = 0;
        top = 0;
        for (int i = 0; i < n; ++i) {
            while (j < n && (!top || s[top - 1] != b[i])) 
                s[top++] = a[j++];
            if (s[top - 1] == b[i]) 
                --top;
        }
        if (!top) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

链接栈

链接栈的概念
链接栈指的是用链接实现的栈。

  • 由于栈的操作都是在栈顶进行的,因此用单链表就足够了
  • 链接栈不需要头结点,因为对栈来讲只需要考虑栈顶元素的插入删除。从栈的基本运算的实现方便性考虑,可将单链表的头指针指向栈顶

在这里插入图片描述

链接栈类定义

template <class elemType>
class linkStack: public stack<elemType> {
    private:
        struct node {
            elemType  data;
            node *next;
            node(const elemType &x, node *N = NULL) { 
                data = x; 
                next = N;
            }
            node():next(NULL) {}
            ~node() {}
        };

        node *top_p;
        
    public:
        // 构造函数:将top_p设为空指针
        linkStack() { top_p = NULL; }	   
        ~linkStack();
        // isEmpty():判top_p是否为空指针
        bool isEmpty() const { return   top_p == NULL; }
        // push(x):在表头执行单链表的插入。
        void push(const elemType &x) { op_p = new node(x, top_p); }
        elemType pop();
        // top():返回top_p指向的结点的值。
        elemType top() const { return top_p->data; }	    
}; 

若一个栈以向量V[1…n]存储,初始栈顶指针top为n+1,则下面x进栈的正确操作是top = top - 1; V[top] = x;

链接栈成员函数实现

  • 析构函数:注意需要释放空间。
template <class elemType>
linkStack<elemType>::~linkStack() {
    node *tmp;

    while (top_p != NULL) {
        tmp = top_p; 
        top_p = top_p ->next;
        delete tmp;
    }
}
  • pop():出栈操作,在表头执行单链表的删除。
template <class elemType>
elemType linkStack<elemType>::pop() {
    node *tmp = top_p;
    elemType x = tmp->data;              

    top_p = top_p->next;                   
    delete tmp;                          
    return x;
} 

链接栈性能分析

  • 由于所有的操作都是对栈顶的操作,与栈中的元素个数无关。所以,所有运算的时间复杂度都是O(1)。

栈的应用

栈的应用1

递归函数的非递归实现
栈的一个主要应用就是把递归函数转换成非递归函数。

递归函数调用过程

  • 递归是一种特殊的函数调用,是在一个函数中又调用了函数本身。
  • 递归程序的本质是函数调用,而函数调用是要花费额外的时间和空间。
  • 在系统内部,函数调用是用栈来实现。

在这里插入图片描述
递归消除方法
如果程序员可以自己控制程序调用的栈,就可以消除递归调用。

方法如下:

定义一个存放子问题的栈
把整个问题放入栈中
While (栈非空)  
    执行解决问题的过程,分解出的小问题进栈

快速排序的非递归实现

  • 快排函数的递归实现:
void quicksort(int a[], int low, int high) {
    int mid;

    if (low >= high) return;

    mid = divide(a, low, high);
    quicksort( a, low, mid-1);
    quicksort( a, mid+1, high);
}
  • 快排非递归实现的思想:
    • 设置一个栈,记录要做的工作,即要排序的数据栈。
    • 栈里面的每个元素为一个node,有leftright两个成员,表示对下标从leftright这一部分数据进行排序。
    • 操作步骤:
先将整个数组进栈
重复下列工作,直到栈空:
    从栈中弹出一个元素,即一个排序区间。
    将排序区间分成两半。
    检查每一半,如果多于两个元素,则进栈。
  • 快排非递归实现的过程:

    • 左边表示要排序的数组,右边表示我们的栈里面的内容。

在这里插入图片描述

  • 快排非递归实现的代码:
// 每个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)}
    }
}

表达式括号匹配

假设一个表达式有英文字母(小写)、运算符+ — * /和左右小(圆)括号构成,以@作为表达式的结束符。
请编写一个程序检查表达式中的左右圆括号是否匹配,若匹配,则返回YES;否则返回NO。表达式长度小于255,左圆括号少于20个。

输入描述:
一行,输入的表达式。

输出描述:
一行,YES或NO

示例 1:
输入:
2*(x+y)/(1-x)@
输出:
YES
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

char s[1010];

int main() {
    int a = 0;
    scanf("%s", s);
    int len = strlen(s);
    for (int i = 0; i < len; i++) {
        if (s[i] =='(') {
            a++;
        }
        if (s[i] == ')') {
            a--;
            if (a < 0) {
                printf("NO");
                return 0;
            }
        }
    }
    if (a == 0)
        printf("YES");
    else
        printf("NO");
    return 0; 
}

栈的应用2

括号配对检查

问题提出:
编译程序的任务之一,就是检查括号是否配对。圆括号、方括号、花括号、单引号和双引号以及注释/*...*/都要配对。

简单地用开括号和闭括号的数量是否相等来判断开括号与闭括号是否配对是不行的。
例如,符号串[()]是正确的,而符号串( [ )]是不正确的。因为当遇到)那样的闭括号时,它与最近遇到开括号匹配。

正确的做法:
遇到右括号时,需要与最近遇到的、没有匹配的左括号匹配。
使用栈,遇到左括号进栈;见遇到右括号,则出栈,并比较两者是否配对。
算法:
1.首先创建一个空栈。
2.从源程序中读入符号。
3.如果读入的符号是开符号,将其进栈。
4.如果读入的符号是一个闭符号但栈是空的,出错。否则,将栈中的符号出栈。
5.如果出栈的符号和和读入的闭符号不匹配,出错。
6.继续从文件中读入下一个符号,非空则转向3,否则执行7。
7.如果栈非空,报告出错,否则括号配对成功。

C++的特殊情况:
至少需要考虑三种括号:圆括号、方括号和花括号。
如果括号出现在注释、字符串常量、字符常量中时,就不需要考虑匹配问题。
在考虑单引号、双引号时,还必须考虑转义字符。

Balance类的设计

  • 设计要求:
    检查一个C++源程序中括号是否配对。
    要求对象初始化时传给它一个源文件名,成员函数checkBalance 检查源文件中的符号是否配对,
    输出所有不匹配的符号及所在的行号和输出总的错误数。

  • 设计考虑:
    数据成员:需要检查的源文件名:文件流对象;行号;错误数
    成员函数:构造函数;检查括号配对函数

//类的定义
class balance 
{
private:
	ifstream fin;          
	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);
 	void SkipQuote(char type);
 	char NextChar();
};
//构造函数
balance :: balance(const char *s)
{
    fin.open(s);
    currentLine = 1;
    Errors = 0;
}

外部代码使用 balance 的时候,仅可调用构造函数以及 CheckBalance 方法。
可以将数据成员 currentLine 和 Errors 作为 CheckBalance 的局部变量,并通过参数的形式传递给其他需要使用到的函数。

CheckBalance的实现

  • 进一步需要细化:
    读文件,直到读入一括号;输出某行某符号不匹配;出错数加1;检查lastChar与match是否匹配。如不匹配,输出出错信息,出错数加1;栈中元素均没有找到匹配的闭符号,输出这些错误。

  • 进一步抽取子函数:

    • 第1项工作: GetNextSymbol
      功能:从文件的当前位置开始,跳过所有的非括号,读到第一个括号后返回。在遇到文件结束时,返回一个特定的符号,如NULL。
      函数原型:函数有一个字符型的返回值。执行该函数必须有一个输入流,在读输入流的过程中,当前处理的行号会变,在读的过程中也可能遇到异常情况,因此出错数也会变。这三个信息:文件流对象,当前处理的行号,出错个数都是对象的数据成员。因此函数原型为:char GetNextSymbol()。
    • 第3项工作:CheckMatch
      功能:比较两个指定位置的待比较的符号是否匹配
      函数原型:bool balance::CheckMatch(char Symb1, char Symb2, int Line1, int Line2 )
//CheckBalance的算法-伪代码
初始化栈为空;
while (lastChar = 读文件,直到读入一有效的括号)
    switch (lastChar) {
    case {', 'T','(': 进栈
    case '}',']',')':
		if (栈空)
			输出某行某符号不匹配;出错数加1;
		else { match = 出栈的符号;
			检查lastChar与match是否匹配;
			如不匹配,输出出错信息,出错数加1;
		}
	}
if(栈非空)栈中元素均没有找到匹配的闭符号,输出这些错误
return 出错数
int balance::CheckBalance()
{
    Symbol node;
    seqStack<Symbol>   st;//seqStack 类中有一个 pop(T) 的成员函数,可以把指定的元素弹
    char LastChar,  Match; 
    while( LastChar = GetNextSymbol() ) {
        switch ( LastChar ) {
            case '(': case '[': case '{':
                node.Token = LastChar;          //todoline1
                node.TheLine = currentLine;     //todoline2这两行可用node = {LastChar, currentLine};替代
                st.push(node);
                break;
            case ')': case ']': case '}': 
                if (st.isEmpty()) {
                    Errors++;
                    cout << "在第" <<  currentLine   << "有一个多余的 " << LastChar << endl;
                }
                else  {
                    node = st.pop();
                    Match = node.Token;
                    if (!CheckMatch( Match, LastChar, node.TheLine, currentLine))
                        ++Errors;
                }
            }        
    }
    while( !st.isEmpty()) {
        Errors++;
        node = st.pop();
        cout << "第" << node.TheLine   << "行的符号" << node.Token   << "不匹配";
    }
    return Errors;
}

GetNextSymbol的实现

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

  • 特殊情况:

    • 注释中的括号不用考虑
    • 字符串常量和字符常量中的括号不用考虑。
    • C++中的注释又有两种形式
      • 以“//”开始到本行结束
      • 以“/”开始到“/”结束,可以跨行
//伪代码
char GetNextSymbol()
{
	while (ch=从文件中读入下一字符){
		if ( ch = = '/') {
			ch= 从文件中读入下一字符;
			if (ch == NULL) break;
			if(ch =='*') 跳过标准C的注释;
			else if (ch == '/') 跳过C++的注释;
			else 不是注释,把ch放回输入流;
	}
	else if ( ch = = '\' || ch == '"') 跳过字符常量或字符串常量;
		else if ( ch = = '{'|| ch = = '['|| ch = = '('|| ch = = ')'|| ch = = ']'|| ch = = '}')
			return ch;
	}
	return 0;
}
char balance :: GetNextSymbol()
{
	char ch;
	while ( ch = NextChar()) {
		if ( ch == '/') {
			if ((ch = NextChar()) == NULL) break;
			if ( ch == '*' ) SkipComment( SlashStar );
			else if ( ch == '/' ) SkipComment( SlashSlash );
			else PutBackChar ( ch);
		}
		else if (ch =='\\'  || ch == '"') SkipQuote(ch);
		//SkipQuote中,当读到转义字符时,直接读入后面一个字符且不做任何检查。那么如果遇到的转义字符不是\’或\”,而是 \127或者 \xFF会不会有问题?不会有问题
		else if ( ch == '{'|| ch == '[' || ch == '('|| ch == ')' || ch == ']' || ch == '}')
			return ch;
	}
	return NULL; // 文件结束。
}

其它函数的实现

  • NextChar 的实现:从输入文件流中读入一个字符,返回给调用程序,遇到文件结束符,返回NULL。如遇到换行符,则当前处理行加1。
char balance :: NextChar()
{
	char ch;
	if ((ch = fin.get()) == EOF)
		return NULL;
	if ( ch == "\n')
		currentLine++;
	return ch;
}
  • PutBackChar 的实现:将传入的字符放回输入流,这是通过调用输入流对象的成员函数putback实现的。如放回的字符是回车,当前处理行号减1。
void balance :: PutBackChar(char ch)
{
	fin.putback(ch) ;
	if ( ch == '\n')
		currentLine --;
}
  • SkipQuote的实现:读文件,直到读到一个和参数值相同的符号。
    • 特殊情况:字符或字符串常量不允许跨行的、转义字符的处理
void balance :: SkipQuote (char type)
{
	char ch;
	while ((ch = NextChar())) {
		if (ch == type) return;
		else if (ch == '\n')
			break ;
		else if (ch == '\l')
			ch = NextChar();
}
Errors++;
cout << "missing closing quote at line "<< currentLine << endl;
}
  • SkipComment的实现:分辨注释类型并完成跳过注释的任务:
    • 如果是以“//”开头的注释,则不断地读文件直到遇到换行或文件结束符
    • 如果是以“/”开头的注释,则必须读到“/”为止。
    • 在第二种注释中,判断注释是否结束要判断连续的两个符号,因此用一个变量flag保存前一次读到的符号。
void SkipComment(enum CommentType type)
{
	if ( type = = SlashSlash ) {
		读文件,直到遇到换行符或文件结束符
		返回
	}
	flag =' '
	while (ch = 从文件中读入一符号){
		if (flag == * && ch == /)
			返回
		flag = ch;
	}
	Errors++, 输出出错信息
}
void balance :: SkipComment(enum CommentType type)
{
	char ch, flag;
	if ( type == SlashSlash ) {
		while ( (ch = NextChar()) && ( ch != '\n') ) ;
		return;
	}
	flag = ' ';
	while( (ch = NextChar() ) != NULL){
		if ( flag == '*' && ch == '/' ) return;
		flag = ch;
	}
	Errors+ +;
	cout << "Comment is unterminated!" < < endl;
}

栈的应用3

后缀表达式

  • 中缀表达式:运算符是处于运算数的中间,如(a + b) * c /( d - e)。

  • 后缀表达式:运算符在运算数后面,上式变为a b + c * d e - /。

  • 后缀表达式的计算:顺序扫描表达式,遇到运算符,将前面离它最近的两个运算数进行运算。

  • 后缀表达式的计算实现:

    • 1.初始化一个栈。
    • 2.依次读入后缀式的操作数和运算符直到结束
      • 若读到的是操作数,则将其进栈。

      • 若读到的是运算符,则将栈顶的两个操作数出栈,后弹出的操作数为被操作数,先弹出的为操作数,将得到的操作数完成运算符所规定的运算,并将结果进栈。

    • 3.当栈中只剩有一个操作数时,弹出该操作数,它就是表达式的值。

中缀转后缀

  • 中缀和对应后缀式的共性:运算数次序不变,根据优先级调整运算符的位置。

  • 中缀转后缀算法(考虑只包含加、减、乘、除运算):

    • 1.遍历中缀表达式

      • 若读入的是操作数,立即输出。
      • 若读入的是闭括号,则将栈中的运算符依次出栈并输出,直到遇到相应的开括号,将开括号出栈。
      • 若读入的是开括号,则进栈。
      • 若读入的是运算符,如果栈顶运算符优先级高于或等于读入的运算符,则栈顶运算符出栈;直到栈顶运算符优先级低于读入的运算符为止,读入的运算符进栈。
    • 2.在读入操作结束时,将栈中所有的剩余运算符依次出栈并输出,直至栈空为止。

计算器类的实现

问题:输入一个中缀表达式,计算的对象为类型为int的正整数,能计算加、减、乘、除和乘方运算,允许用括号改变优先级。

  • calc类的设计:
数据成员:
	一个字符串,用于保存表达式
公有成员函数:
	构造和析构函数
	计算表达式结果
	赋值运算符重载
class calc{
	char *expression;
public:
	calc(const char *s)
	{
		expression = new c;
		strcpy(expression, s);
	}
	~calc(){class expression;}
	calc &operator=(const calc &other)   //函数定义
	{
		if(&other != this)
		{
            ...
		}
		return *this;
	}
	int result();
}

result函数的设计

实现思想:

计算器中的表达式中的运算数都是常量,没有必要先转换成后缀表达式,在计算后缀表达式的值。可以将转换和计算两个步骤合并起来,边转换边计算。 即在中缀转后缀时,发现某个运算符可以出栈时,则直接执行运算。 运算过程需要用到两个栈:中缀表达式转后缀表达式时的运算符栈,执行后缀表达式运算时的运算数栈。

result函数的实现

伪代码:

int calc::result()
{
    依次从表达式中取出一个合法的符号,直到表达式结束 {
        switch(当前符号) {
            case  数字:将数字存入运算数栈;
            case(: 开括号进运算符栈;
            case): 开运算符栈中的运算符依次出栈并执行运算,直到‘(’出栈;
            case^: 乘方运算符进运算符栈;
            case*: case/: 运算符栈中的/*^退栈并执行相应的运算,当前运算符进栈;
            case+:case-: 运算符栈中的运算符依次出栈执行相应的运算,直到栈为空或遇到开括号。当前运算符进栈;
        }
    }  
    运算符栈中在所有的运算符出栈执行;
    if (运算数栈为空) 出错,无运算结果 ;
    result_value = 运算数栈出栈元素;
    if (运算数栈非空) 出错,缺运算符 ;
    return result_value ;
}

考虑进一步细化:在上述伪代码中,大多数的操作都是进栈出栈,这些操作在栈类中都已实现。除此之外,还有两个操作需要细化: 从表达式中取出一个合法的符号:

token getOp(int &value);

执行一个算术运算:

void BinaryOp(token op,  seqStack<int> &dataStack)
int calc :: result()
{
	token lastOp, topOp;
	int result value, CurrentValue;
	seqStack<token> opStack;
	seqStack<int> dataStack;
	char *str = expression;
	while (true}{
		lastOp = getOp(CurrentValue);
		if (lastOp == EOL) break;
		switch (lastOp) {
			case VALUE: dataStack.push(CurrentValue) ; break;
			case CPAREN:
				while( !opStack.isEmpty() && (topOp = opStack.pop()) != OPAREN )
					BinaryOp(topOp, dataStack);
				if ( topOp != OPAREN) cout << "缺左括号" << endl;
				break;
			case OPAREN: case EXP: opStack.push(lastOp); break;
			case MULTI:case DIV:
				while ( !opStack.isEmpty() && opStack.top() >= MULTI)
					BinaryOp(opStack.pop(), dataStack);
			opStack.push(lastOp);break;
			case ADD:case SUB:
				while ( !opStack.isEmpty() && opStack.top() != OPAREN )
					BinaryOp(opStack.pop(), dataStack );
				opStack.push(lastOp);
		}
	}
	while (!opStack.isEmpty())
		BinaryOp(opStack.pop(),dataStack);
	if (dataStack.isEmpty()) {
		cout <<“无结果\n";
		return 0;
	}
	result_value = dataStack.pop():
	if (!dataStack.isEmpty()) {
		cout <<“缺操作符”;
		return 0;
	}
	expression = str;
	return result_value ;
}

私有函数的实现

类中的私有函数:

从表达式中取出一个合法的符号:

token getOp(int &value);

执行一个算术运算:

void BinaryOp(token op, seqStack<int> &dataStack)

getOp的实现:

如果是运算符,根据不同的运算符返回不同的token类型的值。 如果是运算数,则取出这个运算数,转换成整型数存入参数value,返回符号VALUE。 很多程序员在写算术表达式时都习惯在运算符的前后插入一些空格,使表达式看上去更加清晰。这些空格对表达式的计算是没有意义的,在扫描过程中要忽略这些空格。

队列

队列的定义

队列的概念

  • 队列是一种常用的线性结构,到达越早的结点,离开的时间越早。
  • 只能从对队尾入队,从队头出队。
  • 所以队列通常称之为先进先出(FIFO: First In First Out)队列。

在这里插入图片描述

队列的基本操作

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

队列的抽象类

template <class elemType>
class queue{
    public: 
        virtual bool isEmpty() = 0;                  //判队空
        virtual void enQueue(const elemType &x) = 0; //进队
        virtual elemType  deQueue() = 0;             //出队
        virtual elemType getHead() = 0;              //读队头元素
        virtual ~queue() {}                          //虚析构函数
};

队列的顺序实现

顺序队列的概念和设计

  • 顺序队列也是通过一个数组来实现的。

  • 使用数组存储队列中的元素

  • 结点个数最多为MaxSize个 ,下标的范围从0MaxSize-1

  • 顺序队列有三种组织方式:

    • 队头位置固定: 头元素下标为0,并指定尾元素,当队列为空时尾元素被指定为-1。出队会引起大量数据移动。
      在这里插入图片描述

    • 队头位置不固定: 队首指针front指示队首结点的前一位置,队尾指针rear指示队尾结点存放的下标地址,初始化将两指针均设为-1,队满时rear=Maxsize-1。浪费空间。
      在这里插入图片描述

    • 循环队列 : 依旧一个front和一个rear,牺牲一个单元规定front指向的单元不能存储队列元素,只是起到标志作用,表示后面一个是队头元素,可以起到防止队列空和队列满都是front=rear的作用。队列满的条件是:(rear+1)%MaxSize == front。这是相对而言更合理的一种顺序队列的实现方式。
      在这里插入图片描述

循环顺序队列类定义

template <class elemType>
class seqQueue: public queue<elemType> {
    private:
        elemType *elem;
        int maxSize;
        // 队头和队尾
        int front, rear;
        void doubleSpace();
    public:
        seqQueue(int size = 10)// 析构函数:收回动态数组
        ~seqQueue() { delete [] elem ; }
        // 判队列是否为空:队头是否等于队尾
        bool isEmpty() { return front == rear; }
        void enQueue(const elemType &x);
        elemType deQueue();
        // 访问队头元素
        elemType getHead() { return elem[(front + 1) % maxSize]; }
};

循环顺序队列成员函数实现

  • 构造函数
template <class elemType>
seqQueue<elemType>::seqQueue(int size) { 
    elem = new elemType[size];
    maxSize = size; 
    front = rear = 0;
} 

deQueue函数

template <class elemType>
elemType seqQueue<elemType>::deQueue() {
    front = (front + 1) % maxSize;
    return elem[front];
}
  • enQueue函数
template <class elemType>
void seqQueue<elemType>::enQueue(const elemType &x) { 
    if ((rear + 1) % maxSize == front)
        doubleSpace();
    rear = (rear + 1) % maxSize;
    elem[rear] = x;
}
  • doubleSpace函数
template <class elemType>
void seqQueue<elemType>::doubleSpace() {  
    elemType *tmp =elem;

    elem = new elemType[2 * maxSize];
    for (int i = 1; i < maxSize; ++i)
        elem[i] = tmp[(front + i) % maxSize];	 
    front = 0; 
    rear = maxSize - 1;
    maxSize *= 2;
    delete  [] tmp;
} 
解密QQ号
规则是这样的:首先将第1个数删除,紧接着将第2个数放到这串数的末尾,再将第3个数删除并将第4个数再放到这串数的末尾,再将第5个数删除……直到剩下最后一个数,将最后一个数也删除。
按照刚才删除的顺序,把这些删除的数连在一起就是小哈的QQ啦。现在你来帮帮小哼吧。小哈给小哼加密过的一串数是6 3 1 7 5 8 9 2 4。解密后小哈的QQ号应该是6 1 5 9 4 7 2 8 3。

输入描述:
只有2行,第1行有一个整数n (1 ≤ n ≤ 10^5)
第2行有n个整数为加密过的QQ号,每个整数之间用空格隔开。每个整数在 1~9 之间。
对于100%的数据满足1 ≤ n ≤ 10^5。

输出描述:
只有一行,输出解密后的QQ号。

示例 1:
输入:
9
6 3 1 7 5 8 9 2 4
输出:
6 1 5 9 4 7 2 8 3
#include <cstdio> 
#include <cstring> 
#include <iostream> 
#include <algorithm> 
#define N 1000005 
using namespace std; 

int n; 
int q[N], l, r;  

int main() { 
    int n; 
    scanf("%d", &n); 

    l = r = 0; 
    for (int i = 1; i <= n; ++i) { 
        int x; 
        scanf("%d", &x); 
        q[r++] = x; 
    } 

    int flag = 0; 
    while (l < r) { 
        int x = q[l++]; 
        if (!flag) printf("%d ", x); 
        else q[r++] = x; 
        flag ^= 1; 
    } 
    return 0; 
}

链接队列

链接队列的概念和设计

  • 采用不带头结点的单链表
  • 单链表的表头作为队头,单链表的表尾作为队尾
  • 同时记录头尾结点的位置。

在这里插入图片描述

链接队列类定义

template <class elemType>
class linkQueue: public queue<elemType> {
    private:
        struct node {
            elemType  data;
            node *next;
            node(const elemType &x, node *N = NULL){data = x; next = N;}
            node():next(NULL) {}
            ~node() {}
        };
        node *front, *rear; 
    public:
        linkQueue() { front = rear = NULL; }
        // 析构函数,和链表的析构函数类似
        ~linkQueue();      
        bool isEmpty() { return front ==  NULL; }
        void enQueue(const elemType &x);
        elemType deQueue();	  
        elemType getHead() { return front->data; }  
};   

链接队列成员函数实现

  • enQueue函数
template <class elemType>
void linkQueue<elemType>::enQueue(const elemType &x) {
    if (rear == NULL)
        front = rear = new node(x);
    else 
        rear = rear->next = new node(x);
}
  • deQueue函数
template <class elemType>
elemType linkQueue<elemType>::deQueue() {
    node *tmp = front;
    elemType value = front->data;

    front = front->next; 
    delete tmp;
    
    // 判断是否为空
    if (front == NULL) rear = NULL;      
    
    return value;
}

队列的应用

队列应用1:列车车厢重排

问题描述
一列货运列车共有n节车厢,每节车厢将被放在不同的车站。假定n个车站的编号分别为1 – n,货运列车按照第n站到第1站的次序经过这些车站。车厢的编号与它们的目的地相同。为了便于从列车上卸掉相应的车厢,必须重新排列这些车厢,将第n节车厢放在最后,第1节车厢放在最前面。
在这里插入图片描述

实现思想

  • 每条缓冲轨道用一个队列模拟。
  • 处理过程如下:
依次取入轨的队头元素,直到队列为空
    进入一条最合适的缓冲轨道
    检查每条缓冲轨道的队头元素,将可以放入出轨的元素出队,进入出轨

解决方案实现

  • 模拟过程:
// in表示输入轨道中车厢的排列,总共n个车厢,通过k个缓冲轨道
void arrange(int in[], int n, int k) {
    linkQueue<int> *buffer = new linkQueue<int>[k];           
    int last = 0; 
    // 依次把每节车厢放入缓冲轨道,然后检查能否将缓冲轨道中的车厢移动到输出轨道
    for (int i = 0; i < n; ++i) {
        // 如果车厢放不进去,表示重排失败,不必再进行下去
        if (!putBuffer(buffer, k, in[i])) return;          
        checkBuffer(buffer, k, last);
    }
}
  • putBuffer函数:找合适的轨道
    • 基本要求:轨道上最后一节车厢的编号小于进入的车厢编号,没有满足基本要求的轨道,则启用一条新轨道
    • 最优要求:多个轨道满足时,选择其中最后一节车厢编号最大的一个
    • 例如处理车厢8,现有轨道情况
      • 轨道1: 2、5
      • 轨道2:3、7
      • 8应该放入轨道2。如果放入轨道1,接入后面一节车厢是6号,则必须启用一根新的轨道
// 把车厢in,放入缓冲轨道,buffer是存储缓冲轨道队列的数组,size表示缓冲轨道的数量
// 如果车厢能放入合适的缓冲轨道则返回true,否则返回false

bool putBuffer(linkQueue<int> *buffer, int size, int in) { 
    // 希望把车厢放到编号为avail的缓冲轨道上,max表示所有合适轨道队尾编号最大的车厢
    int avail = -1, max = 0;     

    for (int j = 0 ; j < size; ++j)  {        
        if (buffer[j].isEmpty()) { if (avail == -1) avail = j; }  
        else if (buffer[j].getTail() < in && buffer[j].getTail() > max) {          
            avail = j;
            max = buffer[j].getTail();
        }
    }
    if (avail != -1) {          
        buffer[avail].enQueue(in);
        cout << in << "移入缓冲区 " << avail << endl;
        return true;
    } else {
        cout << "无可行的方案" << endl;
        return false;
    }
}
  • checkBuffer函数
// 检查能否将缓冲轨道上的车厢移动到输出轨道,last代表输出轨道上最后一列车厢的标号

void checkBuffer( linkQueue<int> *buffer, int size, int &last) {
    // 表示需要检查缓冲轨道
    bool flag = true;    
    while (flag) {       
        flag = false;
        for (int j = 0; j < size; ++j) {    
            if (! buffer[j].isEmpty() &&  buffer[j].getHead() == last + 1) { 
                cout << "将" << buffer[j].deQueue() << "从缓冲区" << j  << "移到出轨" << endl;
                ++last;
                // 有车厢出轨后再次将flag设置为true
                flag = true;
                break;   
            }
        }
    }
}

队列应用2:排队系统模拟

排队系统

事件:到达事件 离开事件
模拟:模拟每一秒发生的事件 时间驱动的模拟
特点:事件不是连续发生的,没有必要处理没有事件发生的时间
优化方案-事件驱动的模拟:把时间按发生时间排序,依次处理每一个事件

排队系统的模拟

生成所有的到达事件
按事件的发生时间不断处理事件
    ·到达事件:如果服务员没空,顾客到队列去排队。否则为这个顾客生成服务所需的时间t。经过了t 时间以后会产生一个顾客离开事件。
    ·离开事件:检查有没有顾客在排队。如果有顾客在排队,则让队头顾客离队,为他提供服务。如果没顾客排队,则服务员可以休息。

可以在模拟过程中统计客户的排队长度、等待时间、服务员的连续工作时间、空闲时间等统计信息。

事件生成

到达时间间隔和为每个顾客的服务时间并不一定是固定的。
尽管服务时间和到达的间隔时间是可变的,但从统计上来看是服从一定的概率分布。
要生成到达时间或服务时间必须掌握如何按照某个概率生成事件

均匀分布事件生成

如顾客到达的间隔时间服从[a,b]之间的均匀分布,则可以生成一个[a,b]之间的一个随机数x,表示前一个顾客到达后,经过了x的时间后又有一个顾客到达了。
生成随机数可以用 rand() * (b – a + 1)/(RAND_MAX + 1) + a

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

问题

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

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

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

一共模拟customNum个顾客

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

事件存储: 根据到达早离开早的特点可以用FIFO存储到达事件。

模拟过程

生成所有的顾客到达事件,按到达时间排成一个队列;
设置当前时间为0
    ·依次处理队列中的每个元素,直到队列为空
    ·检查顾客的到达时间和当前时间,计算等待时间,记入累计值;
    ·生成顾客服务时间
    ·将当前时间拨到该顾客的离开时间
返回累计值除以顾客数的结果。

伪代码

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

类定义

class simulator{
	int arrivalLow;
	int arrivalHigh;
	int serviceTimeLow;
	int serviceTimeHigh;
	int customNum;
public:
	simulator();
	int avgWaitTime() const;
}; 

构造函数

simulator::simulator() 
{
	cout << "请输入到达时间间隔的上下界:";
	cin >> arrivalLow >> arrivalHigh;
	cout << "请输入服务时间的上下界:";
	cin >> serviceTimeLow >> serviceTimeHigh;
	cout << "请输入模拟的顾客数:";
	cin >> customNum;
	srand(time(NULL));
} 

avgWaitTime函数

int simulator::avgWaitTime() const
{
     int currentTime = 0,  totalWaitTime = 0,  eventTime;
     linkQueue<int>  customerQueue;
 
     for (int i = 0; i < customNum;  ++i) {
          currentTime += arrivalLow +   (arrivalHigh -arrivalLow +1) * rand() / (RAND_MAX + 1);
          customerQueue.enQueue(currentTime);
     }  
     currentTime = 0;
     while (!customerQueue.isEmpty()) {
          eventTime = customerQueue.deQueue();
          if (eventTime < currentTime)    totalWaitTime += currentTime - eventTime;
          else currentTime = eventTime;    
          currentTime += serviceTimeLow 
                                +   (serviceTimeHigh -serviceTimeLow +1) * rand() / (RAND_MAX + 1);
    }
   return totalWaitTime / customNum;
} 

avgWaitTime

#include "simulator.h"
#include <iostream>
using namespace std;
int main()
{ 
     simulator sim;
     cout << "平均等待时间:“
          << sim.avgWaitTime() << endl;
     return 0;
} 
小组队列
有m个小组,n个元素,每个元素属于且仅属于一个小组。

支持以下操作:
push x:使元素x进队,如果前边有x所属小组的元素,x会排到自己小组最后一个元素的下一个位置,否则x排到整个队列最后的位置。
pop:出队,弹出队头并输出出队元素,出队的方式和普通队列相同,即排在前边的元素先出队。

对于全部测试数据,保证1 ≤ n ≤ 10^5,1 ≤ m ≤ 300,T ≤ 10^5,输入保证操作合法。

输入描述:
第一行有两个正整数n m,分别表示元素个数和小组个数,元素和小组均从0开始编号。
接下来一行n个非负整数Ai,表示元素i所在的小组。
接下来一行一个正整数T,表示操作数。
接下来T行,每行为一个操作。

输出描述:
对于每个出队操作输出一行,为出队的元素。

示例 1:
输入:
4 2
0 0 1 1
6
push 2
push 0
push 3
pop
pop
pop
输出:
2
3
0
#include <cstdio> 
#include <cstring> 
#include <iostream> 
#include <algorithm> 
#define N 100005 
#define M 305 
using namespace std; 

int n, m, t; 
int team[N]; 
int q[M][N], l[M], r[M]; 

int main() { 
    scanf("%d%d", &n, &m); 
    for (int i = 0; i < n; ++i) scanf("%d", &team[i]); 

    scanf("%d", &t); 
    char op[10]; 
    int x; 
    while (t--) { 
        scanf("%s", op); 
        if (op[1] == 'u') { 
            scanf("%d", &x); 
            int tx = team[x]; 
            if (l[tx] == r[tx]) q[m][r[m]++] = tx; 
            q[tx][r[tx]++] = x; 
        } else { 
            int tx = q[m][l[m]]; 
            printf("%d\n", q[tx][l[tx]++]); 
            if (l[tx] == r[tx]) ++l[m]; 
        } 
    } 
    return 0; 
}

字符串

字符串定义

  • 字符串是由零个或多个字符组成的有限序列,一般记为 s = “ a 0 a 1 a 2 … a n − 1 ” s=“a_0 a_1 a_2…a_{n-1}” sa0a1a2an1,n称为字符串的长度
  • 将每个 a i a_i ai看成一个元素,则字符串可看成一个线性表。
  • 字符串的特点:需要对整个表而不是表中元素进行操作

字符串操作

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

字符串存储

  • 顺序存储
    • 字符数组存储字符串。
      • C语言的处理方式:固定大小的数组(可以调用cstring库,处理字符串)
      • C++的处理方式:动态改变数组大小(使用string类)
    • 缺点:插入子串、删除子串的时间性能差。
  • 链接存储
    • 单链表存储字符串。
    • 缺点:空间问题,1个字符1个字节,1个指针可能要占4个字节
  • 块状链表存储字符串。

块状链表的概念和操作
概念: 块状链表的每个结点存放字符串中一段连续的字符,而不是单个字符。
在这里插入图片描述

  • 优点:提高了空间的利用率
  • 缺点:插入和删除时会引起数据的大量移动
  • 改进方法:允许节点有一定的空闲空间(空间换时间)

块状链表删除:

  • 分裂起始和终止结点
    在这里插入图片描述

  • 删除中间结点
    在这里插入图片描述

  • 归并起始和终止结点
    在这里插入图片描述

块状链表插入:

  • 分裂节点
    在这里插入图片描述

  • 插入子串
    在这里插入图片描述

  • 归并结点
    在这里插入图片描述

墓碑字符
考古学家发现了一座千年古墓,墓碑上有神秘的字符。经过仔细研究,发现原来这是开启古墓入口的方法。

墓碑上有两行字符串,其中第一个串的长度为偶数,现在要求把第二个串插入到第一个串的正中央,如此便能开启墓碑进入墓中。

输入描述:
第一行一个整数n,表示测试数据的组数。
接下来n组数据,每组数据两行。每行各一个字符串,且长度满足1 ≤ length ≤ 50,并且第一个串的长度必为偶数。

输出描述:
每组数据输出一个能开启古墓的字符串,每组输出占一行。

示例 1:
输入:
2
CSJI
BI
AB
CMCLU
输出:
CSBIJI
ACMCLUB

#include <iostream>
#include <string>

using namespace std; 

int main() {
    int t, len;
    cin >> t;
    while (t--) {
        string a, b, ans;
        cin >> a >> b;
        len = a.length();
        ans = a.substr(0, len / 2) + b + a.substr(len / 2);
        cout << ans << endl;
    }
    return 0;
}

树与二叉树

树的定义

树的概念

  • 树的定义

    • 树是 n ( n ≥ 1 ) n (n \geq 1) n(n1)个结点的有限集合 T T T
    • 有一个被称之为根(root)(root)的结点
    • 其余的结点可分为 m ( m ≥ 0 ) m(m \geq 0) m(m0)个互不相交的集合 T 1 , T 2 , … , T m T_1,T_2,…,T_m T1T2Tm,这些集合本身也是一棵树,并称它们为根结点的子树 ( S u b r e e ) (Subree) (Subree)。每棵子树同样有自己的根结点。
  • 树形结构的特点

    • 一个数据元素可以有多个直接后继,但只有至多一个直接前驱
    • 因为线性结构中每个数据元素至多只有一个直接前驱和直接后继,因此树形结构可以表达更复杂的关系。
      在这里插入图片描述
  • 一些术语

    • 根结点,叶结点,内部结点:
      • 树中唯一一个没有直接前驱的结点称为根节点。树中没有直接后继的结点称为叶结点。除根以外的非叶结点称为内部结点。
    • 结点的度和树的度:
      • 一个结点直接后继的数目称为结点的度。树中所有结点的度的最大值称为这棵树的度。
  • 子结点,父结点,祖先结点,子孙结点:

    • 结点的直接后继称为结点的子结点。
    • 结点的直接前驱称为结点的父结点。
    • 在树中,每个结点都存在着唯一的一条到根结点的路径,路径上的所有结点都是该结点的祖先结点。
    • 子孙结点是指该点的所有子树中的全部结点。也就是说,树中除根结点以外的所有结点都是根结点的子孙结点。
  • 兄弟结点:

    • 同一个结点的子结点互为兄弟结点。
  • 结点的层次、高度和树的高度:

    • 结点的层次,也称为深度,是从根结点到这个结点所经过的边数。
    • 一棵树中结点的最大层次称为树的高度或深度。
    • 结点的高度指的是以该结点为根的子树的高度。
  • 有序树和无序树:

    • 若将树中每个结点的子树看成自左向右有序的,则称该树为有序树,否则称为无序树。
    • 在有序树中,最左边的子树称为第一棵子树,最右边的子树称为最后一棵子树。
  • 森林:

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

树的基本操作

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

树的抽象类

template<class T>
class tree {
public:
    virtual void clear() = 0;
    virtual bool isEmpty() const = 0;
    virtual int size() = 0;
    virtual T root(T flag) const = 0;
    virtual T parent(T x, T flag) const = 0; 
    virtual T child(T x, int i, T flag) const = 0;
    virtual void remove(T x, int i) = 0;
    virtual void traverse() const = 0;
    virtual ~tree() {}
};

二叉树的定义

二叉树的概念

  • 二叉树(Binary Tree)是结点的有限集合,它或者为空,或者由一个根结点及两棵互不相交的左、右子树构成,而其左、右子树又都是二叉树。
    • 注意:二叉树必须严格区分左右子树。即使只有一棵子树,也要说明它是左子树还是右子树。交换一棵二叉树的左右子树后得到的是另一棵二叉树。
      在这里插入图片描述
  • 满二叉树
    • 一棵高度为 k k k并具有 2 k - 1 2^k-1 2k1个结点的二叉树称为满二叉树。
  • 完全二叉树:
    • 在满二叉树的最底层自右至左依次(注意:不能跳过任何一个结点)去掉若干个结点得到的二叉树也被称之为完全二叉树。满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
  • 特点:
    • 所有的叶结点都出现在最低的两层上。
    • 对任一结点,如果其右子树的高度为 k k k,则其左子树的高度为 k k k k + 1 k+1 k1
      在这里插入图片描述

二叉树的基本操作

  • 建树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():层次遍历二叉树上的每个结点
    • 先访问根结点,然后按从左到右的次序访问第二层的结点。在访问了第k层的所有结点后,再按从左到右的次序访问第k+1层。以此类推,直到最后一层。

我们可以通过前序+中序遍历确定一棵二叉树

  • 找出根结点,区分左右子树
  • 继续对左右子树重复这个过程
  • 右子树只有根结点
  • 找出左子树的前序、中序序列

二叉树抽象类

template<class T>
class bTree {
public:
    virtual void clear() = 0;
    virtual bool isEmpty() const = 0;
    virtual int size() const = 0;
    virtual T Root(T flag) const = 0;
    virtual T parent(T x, T flag) const = 0; 
    virtual T lchild(T x, T flag) const = 0;
    virtual T rchild(T x, T flag) const = 0;
    virtual void delLeft(T x) = 0;
    virtual void delRight(T x) = 0;
    virtual void preOrder() const = 0;
    virtual void midOrder() const = 0;
    virtual void postOrder() const= 0;
    virtual void levelOrder() const = 0;
    virtual bTree() {}
};

二叉树的性质

  • 一棵非空二叉树的第 i i i 层上最多有 2 i − 1 2^{i - 1} 2i1 个结点 ( i ≥ 1 ) (i \geq 1) i1

    • 用数学归纳法证明。
  • 一棵高度为 k k k的二叉树,最多具有 2 k - 1 2^k-1 2k1结点。

    • 根据性质1可以推断得到。
  • 对于一棵非空二叉树,如果叶子结点数为 n 0 n_0 n0,度数为 2 2 2的结点数为 n 2 n_2 n2,则有: n 0 = n 2 + 1 n_0=n_2+1 n0n21成立。

  • 具有 n n n个结点的完全二叉树的高度为 k = ⌊ l o g 2 n ⌋ + 1 k = \lfloor log_{2}n \rfloor + 1 k=log2n+1

  • 非常有用:如果对一棵有 n n n个结点的完全二叉树中的结点按层自上而下(从第 1 1 1层到第 ⌊ l o g 2 n ⌋ + 1 \lfloor log_{2}n \rfloor +1 log2n+1层),每一层按自左至右依次编号。若设根结点的编号为 1 1 1。则对任一编号为ii的结点 ( 1 ≤ i ≤ n ) (1≤i≤n) (1in),有:

    • 如果 i = 1 i=1 i1,则该结点是二叉树的根结点;如果 i > 1 i>1 i>1,则其父亲结点的编号为 ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor 2i
    • 如果 2 i > n 2i > n 2i>n,则编号为ii的结点为叶子结点,没有儿子;否则,其左儿子的编号为 2 i 2i 2i
    • 如果 2 i + 1 > n 2i + 1 > n 2i+1>n,则编号为 i i i的结点无右儿子;否则,其右儿子的编号为 2 i + 1 2i+1 2i+1

二叉树的顺序存储

二叉树可以顺序存储或者链接存储

顺序存储的概念
顺序存储就是用数组存储二叉树。

  • 完全二叉树的顺序存储:按层编号把层次关系映射到线性关系。
  • 非完全二叉树的顺序存储:将普通的树修补成完全二叉树,按层编号把层次关系映射到线性关系,用特殊的值表示“假”结点。
    在这里插入图片描述

顺序存储的特点

  • 浪费空间,比如极端情况:只有右儿子。

  • 因此一般只用于一些特殊的场合,如静态的并且结点个数已知的完全二叉树或接近完全二叉树的二叉树。

顺序存储二叉树的节点数量统计
现有一棵根节点为1且按照顺序存储的完全二叉树。我们已知完全二叉树的最后一个结点是n。我们想知道,以结点m为根节点,其所在的子树中一共包括多少个结点。

输入描述:
一行两个整数n m。
对于100%的数据满足1 ≤ m ≤ n ≤ 10^5。

输出描述:
输出一行,该行包含一个整数,表示结点m所在子树中包括的结点的数目。

示例 1:
输入:
7 3
输出:
3
#include <iostream>

using namespace std; 

int n, m, ans;

void getAns(int pos) {
    ans++;
    if (pos * 2 <= n)
        getAns(pos * 2);
    if (pos * 2 + 1 <= n)
        getAns(pos * 2 + 1);
    return;
}

int main() {
    cin >> n >> m;
    getAns(m);
    cout << ans;
    return 0;
}

二叉树的链接存储

二叉树的链接存储有两种方式。

  • 二叉链表 指出儿子结点:类似单链表。
    在这里插入图片描述

  • 三叉链表 同时指出父亲和儿子结点:类似双链表。
    在这里插入图片描述

由于在二叉树中很少存在通过儿子找父亲的操作,所以我们常使用二叉链表进行存储,二叉链表也被称作二叉树的标准存储方式。

子树的大小
有一棵有n个结点的二叉树,节点编号为1 ~ n,已知所有结点的左右子节点和根节点的编号r,现在我们想知道每个节点所在的子树中一共包括多少个结点。

对于100%的数据满足1 ≤ r ≤ n ≤ 10^5。

输入描述:
第一行两个整数n r,分别表示节点的数量和根节点的编号。
接下来n行,每行两个整数,第i行表示节点i的左右子树,若为0则表示为空。

输出描述:
n行,每行一个整数,第i行表示以结点i为根节点,其所在的子树中一共包括多少个结点。

示例 1:
输入:
5 1
2 3
4 5
0 0
0 0
0 0
输出:
5
3
1
1
1
#include <iostream>

#define N 100010

using namespace std; 

int lson[N], rson[N], val[N];

void getAns(int pos) {
    if (lson[pos])
        getAns(lson[pos]);
    if (rson[pos])
        getAns(rson[pos]);
    val[pos] = val[lson[pos]] + val[rson[pos]] + 1;
    return;
}

int main() {
    int n, r;
    cin >> n >> r;
    for (int i = 1; i <= n; i++)
        cin >> lson[i] >> rson[i];
    getAns(r);
    for (int i = 1; i <= n; i++)
        cout << val[i] << endl;
    return 0;
}

二叉树链表类设计

数据成员设计
由两个类组成:

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

成员函数设计

  • 实现抽象类规定的所有函数;
  • 二叉树是递归定义,所以操作可用递归实现:
    • 递归函数必须有一个控制递归终止的参数;
    • 每个需要递归实现的公有成员函数对应一个私有的、带递归参数的成员函数;
    • 公有函数调用私有函数完成相应的功能;
  • 有些操作需要先找到某个结点的地址。
    • 增设一个私有成员函数find

二叉链表类定义

template<class T>
class binaryTree : public bTree<T> {
    friend void printTree(const binaryTree &t, T flag);
    private:
        struct Node {                          //二叉树的结点类
            Node  *left , *right ;               
            T data;                         
            Node() : left(NULL), right(NULL) { }
            Node(T item, Node *L = NULL, Node * R =NULL) : data(item), left(L), right(R) { }
            ~Node() {} 
        };
        Node *root;
    public:
        // 创造空二叉树
        binaryTree() : root(NULL) {}
        // 创造只有根结点的二叉树
        binaryTree(T x) { root = new Node(x); }
        ~binaryTree()  { clear() ; }
        void clear() ;
        bool isEmpty() const{return root == NULL;}
        int size() const;
        T Root(T flag) const;
        T lchild(T x, T flag) const;
        T rchild(T x, T flag) const; 
        void delLeft(T x) ;
        void delRight(T x);
        void preOrder() const;
        void midOrder() const;
        void postOrder() const;
        void levelOrder() const;
        void createTree(T flag);
    private:
        Node *find(T x, Node *t ) const;
        // 同名公有函数递归调用以下函数实现
        void clear(Node *&t);
        int size(Node *t) const; 
        void preOrder(Node *t) const;
        void midOrder(Node *t) const;
        void postOrder(Node *t) const;
}; 

二叉链表成员函数实现

  • root函数的实现
    • 返回root指向的结点的数据部分。如果二叉树是空树,则返回一个特殊的标记。
template <class T>
T binaryTree<T>::Root(T flag) const {
    if (root == NULL) return flag;
    else return root->data;
}
  • size函数的实现
    • 一棵二叉树由三部分组成:根结点、左子树、右子树。
    • 树的结点数 = = = 左子树结点数 + + + 右子树结点数 + 1 + 1 +1
    • 计算左右子树的结点数需要递归调用本函数
template<class T>
int binaryTree<T>::size(binaryTree<T>::Node *t) const {
    if (t == NULL) return 0;
    else return size(t->left) + size(t->right) + 1;
}
template<class T>
int binaryTree<T>::size() const {
    return  size(root);
} 
  • clear函数的实现
    • 一棵二叉树由三部分组成:根结点、左子树、右子树。
    • 删除一棵树 = 删除左子树 + 删除右子树 + 删除根
    • 删除左右子树需要递归调用本函数
template<class T>
void binaryTree<T>::clear(binaryTree<T>::Node *&t) {
    if (t == NULL)  return;
    clear(t->left);
    clear(t->right);
    delete t;
    t = NULL;
}
template<class T>
void binaryTree<T>::clear() {
    clear(root);
} 
  • 前序遍历的实现
    • if (空树) return;
    • 访问根结点;
    • 前序遍历左子树;
    • 前序遍历右子树;
template<class T>
void binaryTree<T>::preOrder(binaryTree<T>::Node *t) const { 
    if (t == NULL) return;
    cout << t->data << ' ';
    preOrder(t->left);
    preOrder(t->right);
}
template<class T>
void binaryTree<T>::preOrder()  const { 
    cout << "\n前序遍历:";
    preOrder(root);
}
  • 中序遍历的实现
    • if (空树) return;
    • 中序遍历左子树;
    • 访问根结点;
    • 中序遍历右子树;
template<class T>
void binaryTree<T>::midOrder (binaryTree<T>::Node *t) const {
    if (t == NULL) return;
    midOrder(t->left);
    cout << t->data << ' ';
    midOrder(t->right);
}
template<class T>
void binaryTree<T>::midOrder() const {  
    cout << "\n中序遍历:";
    midOrder(root);
}    
  • 后序遍历的实现
    • if(空树)return;
    • 访问根结点;
    • 后序遍历左子树;
    • 后序遍历右子树;
template<class T>
void binaryTree<T>::postOrder (binaryTree<T>::Node *t) const {
    if (t == NULL) return;
    postOrder(t->left);	
    postOrder(t->right);
    cout << t->data << ' ';
}	
template<class T>
void binaryTree<T>::postOrder() const {  
    cout << "\n后序遍历:";
    postOrder(root);	
}
  • 层次遍历的实现
    • 访问过程
      • 根节点
      • 根节点的儿子
      • 根节点的儿子的儿子
  • 关键问题
    • 如何储存已经可以被访问的结点
    • 使用队列
  • 工作过程:
根结点入队
While(队列非空) {
    出队并访问
    将儿子入队
}
template<class T>
void binaryTree<T>::levelOrder() const {
    linkQueue< Node * > que;
    Node *tmp;
    
    cout << “\n层次遍历:”;
    que.enQueue(root);	
    while (!que.isEmpty()) {
        tmp = que.deQueue();
        cout << tmp->data << ' ';
        if (tmp->left)
            que.enQueue(tmp->left);
        if (tmp->right)
            que.enQueue(tmp->right);
    }
}
  • find的实现
    • 如何找结点x:遍历,可以采用任一种遍历
    • 工作过程:采用前序遍历
if (根结点是x) 返回根结点地址
tmp = 左子树递归调用find函数
if (tmp不是空指针) return tmp;
else return 对右子树递归调用find的结果
binaryTree<T>::Node *binaryTree<T>::find(T x, binaryTree<T>::Node *t) const {
    Node *tmp;
    if (t == NULL) return NULL;
    if (t->data == x) return t;   
    if (tmp = find(x, t->left) ) return tmp;
    else return find(x, t->right);
}
  • lchild的实现
    • 返回结点x的left值
template <class T>
T binaryTree<T>::lchild(T x, T flag) const {
    Node * tmp = find(x, root);

    if (tmp == NULL || tmp->left == NULL)
        return flag;
    
    return tmp->left->data;
}
  • rchild的实现
    • 返回结点x的right值
template <class T>
T binaryTree<T>::rchild(T x, T flag) const {
    Node * tmp = find(x, root);

    if (tmp == NULL || tmp->right == NULL)
        return flag;

    return tmp->right->data;
}
  • delLeft的实现
    • 对左子树调用clear函数删除左子树,然后将结点x的left置为NULL。
template <class T>
void binaryTree<T>::delLeft(T x) {
    Node *tmp = find(x, root);

    if (tmp == NULL) return;
    clear(tmp->left);
}
  • delRight的实现
    • 对右子树调用clear函数删除右子树,然后将结点x的right置为NULL。
template <class T>
void binaryTree<T>::delLeft(T x) {
    Node *tmp = find(x, root);

    if (tmp == NULL) return;
    clear(tmp->left);
}
  • createTree创建一棵树
    • 创建过程
      • 按层次遍历输入结点
      • 先输入根结点;对已输入的每个结点,依次输入它的两个儿子的值。如果没有儿子,则输入一个特定值
template <class Type>
void BinaryTree<Type>::createTree(Type flag) { 
    linkQueue<Node *> que;
    Node *tmp;
    Type x, ldata, rdata;

    cout << "\n输入根结点:";
    cin >> x;
    root = new Node(x);
    que.enQueue(root);
    while (!que.isEmpty()) {
        tmp = que.deQueue();
        cout << "\n输入" << tmp->data   << "的两个儿子(" << flag   << "表示空结点):";
        cin >> ldata >> rdata;
        if (ldata != flag)
            que.enQueue(tmp->left = new Node(ldata));
        if (rdata != flag)   
            que.enQueue(tmp->right = new Node(rdata));
    }
    cout << "create completed!\n";
} 	
  • printTree输出一棵树
    • 以层次遍历的次序输出每个结点和它的左右孩子
template <class T>
void printTree(const binaryTree<T> &t, T flag) {
    linkQueue<T> q;

    q.enQueue(t.root->data);
    cout << endl;
    while (!q.isEmpty()) {
        char p, l, r;
        p = q.deQueue();
        l = t.lchild(p,  flag);
        r = t.rchild(p,  flag);
        cout << p << "  " << l  << "  " << r << endl;
        if (l != flag) q.enQueue(l);
        if (r != flag) q.enQueue(r);
    }
}   
二叉树的遍历
有一棵有n个结点的二叉树,节点编号为1 ~ n,已知所有结点的左右子节点和根节点的编号r,现在我们想知道这棵二叉树的前序遍历,中序遍历和后序遍历结果。

输入描述:
第一行两个整数n r (1 ≤ r ≤ n ≤ 10^5),分别表示节点的数量和根节点的编号。
接下来n行,每行两个整数,第i行表示节点i的左右子树,若为0则表示为空。

输出描述:
三行,每行n个整数,整数间用一个空格隔开。
第一行表示这棵二叉树的前序遍历,第二行表示这棵二叉树的中序遍历,第三行表示这棵二叉树的后序遍历。

示例 1:
输入:
5 1
2 3
4 5
0 0
0 0
0 0
输出:
1 2 4 5 3
4 2 5 1 3
4 5 2 3 1
#include <iostream>

#define N 100010

using namespace std; 

int lson[N], rson[N];

void getAns1(int pos) {
    cout << pos << " ";
    if (lson[pos])
        getAns1(lson[pos]);
    if (rson[pos])
        getAns1(rson[pos]);
    return;
}

void getAns2(int pos) {
    if (lson[pos])
        getAns2(lson[pos]);
    cout << pos << " ";
    if (rson[pos])
        getAns2(rson[pos]);
    return;
}

void getAns3(int pos) {
    if (lson[pos])
        getAns3(lson[pos]);
    if (rson[pos])
        getAns3(rson[pos]);
    cout << pos << " ";
    return;
}

int main() {
    int n, r;
    cin >> n >> r;
    for (int i = 1; i <= n; i++)
        cin >> lson[i] >> rson[i];
    getAns1(r);
    cout << endl;
    getAns2(r);
    cout << endl;
    getAns3(r);
    cout << endl;
    return 0;
}

优先级队列

优先级队列的定义

优先级队列的概念

  • 队列是一个先进先出的线性表;而优先级队列指的是:结点之间的关系是由结点的优先级决定的,而不是由入队的先后次序决定。优先级高的先出队,优先级低的后出队。
  • 优先级队列可以基于线性结构实现
    • 按照 优先级 排序
      • 入队:按照优先级插入在合适的位置 - O(N)
      • 出队:队头元素 - O(1)
    • 按照 到达时间 排序
      • 入队:插入到队尾 - O(1)
      • 出队:寻找优先级最高的并删除 - O(N)
  • 优先级队列也可以基于二叉堆实现
    • 二叉堆指的是父子之间的大小满足一定约束的完全二叉树
      • 最小化堆:父结点的值小于等于儿子结点
      • 最大化堆:父结点的值大于等于儿子结点
    • 出队:删除根结点 - O(logN)
    • 入队:在最后一层的第一个空位置上添加一个元素,但添加后要调整元素的位置,以保持堆的有序性 - O(logN)
      在这里插入图片描述

基于二叉堆的优先级队列设计

  • 可以采用顺序存储;
  • 数据成员包括:
    • 队列长度
    • 指向数组起始地址的指针
    • 数组大小
  • 成员函数包括:
    • 抽象类规定的所有功能
      • 创建一个队列:create()
      • 入队:enQueue(x)
      • 出队:deQueue()
      • 读队头:getHead
      • 判队空:isEmpty
  • 私有的工具函数
    • 扩展数组的工具函数doubleSpace
    • 构造时的工具函数buildHeap
    • 出队时的工具函数percolateDown

基于二叉堆的优先级队列类定义

template <class Type>
class priorityQueue:public queue<Type> {
    private:
        // 队列长度
        int currentSize;  
        // 指向数组起始地址的指针
        Type *array; 
        // 数组大小
        int maxSize;
        void doubleSpace();
        void buildHeap();
        void percolateDown(int hole);  
    public:
        priorityQueue(int capacity = 100);
        priorityQueue(const Type data[], int size);
        ~priorityQueue() { delete [] array; }
        bool isEmpty() const { return currentSize == 0; }
        void enQueue(const Type & x);
        Type deQueue();
        Type getHead() { return array[1]; }
};

// 构造函数 - 构造空的优先级队列
priorityQueue(int capacity = 100) { 
    array = new Type[capacity];
    maxSize = capacity;   
    currentSize = 0;
}

入队和出队操作

对于基于二叉堆实现的优先级队列,我们可以这样实现入队和出队的操作

入队 enque

  • 操作要求
    • 保证结构性:插入在最底层的第一个空位
    • 保证有序性:向上移动到合适的位置(向上过滤)
  • 实现
template <class Type>
void priorityQueue<Type>::enQueue( const Type & x ) {
    if(currentSize == maxSize - 1) 
        doubleSpace();

    int hole = ++currentSize;
    for(; hole > 1 && x < array[hole / 2]; hole /= 2)
        array[hole] = array[hole / 2];
    array[hole] = x;
} 
  • 复杂度
    • 平均是2.6次比较,因此元素平均上移1.6层。
    • 最坏是对数的时间复杂度

出队 deque

  • 操作要求
    • 保证结构性:将最后一个结点移到根结点
    • 保证有序性:向下移动到合适的位置(向下过滤)
  • 实现
template <class Type>
Type priorityQueue<Type>::deQueue() { 
    Type minItem;
    minItem = array[1];
    array[1] = array[currentSize--];
    // 从1号下标开始向下过滤
    percolateDown(1);
    return minItem;
} 

template <class Type>
void priorityQueue<Type>::percolateDown(int hole) { 
    int child;
    Type tmp = array[hole];

    for(; hole * 2 <= currentSize; hole = child) { 
        child = hole * 2;
        if (child != currentSize && array[child + 1] < array[child])
            child++;
        if( array[child] < tmp )   
            array[hole] = array[child];
        else break;
    }
    array[hole] = tmp;
}
  • 复杂度
    • 最坏和平均都是对数的时间复杂度
干员招募
医药公司骡德岛在进行干员招募,负责招募的刀客塔将每位干员的简历都标上了优先级,数字越大优先级也越高。
可是骡德岛的需求有限,所以招募时会优先录用优先级最高的干员。
你叫艾拉法雅,作为人力资源联络员的你,现在将接受刀客塔的指令,协助刀客塔完成招募工作。刀客塔只会有两种行动:

递给你一份标有优先级的简历,你将这份简历放到你的办公桌上。
告诉你招募一个干员,这时,你需要在你的办公桌上找到优先级最高的一份简历递出去,并且同时记录这份简历的优先级。

输入描述:
第一行一个整数m (1 ≤ m ≤ 10^5),表示依次插入刀客塔行动的次数。
接下来m行,每行表示刀客塔的一次行动。
若该行为1 x,则表示刀客塔递给你一份简历,并且其优先级为x。
若该行为2,则表示招募干员。

输出描述:
若干行,每行一个整数,表示依次递出简历的优先级。

示例 1:
输入:
5
1 1
1 10
1 2
2
2
输出:
10
2
#include <iostream>
#include <algorithm>

#define N 100010

using namespace std; 

int val[N], cnt;

void up(int pos) {
    while (pos != 1) {
        int pre = pos / 2;
        if (val[pos] > val[pre]) {
            swap(val[pos], val[pre]);
            pos = pre;
        }
        else
            break;
    }
    return;
}

void down(int pos) {
    if (val[pos] >= val[pos * 2] && val[pos] >= val[pos * 2 + 1])
        return;
    if (val[pos * 2] > val[pos * 2 + 1]) {
        swap(val[pos], val[pos * 2]);
        down(pos * 2);
    }
    else {
        swap(val[pos], val[pos * 2 + 1]);
        down(pos * 2 + 1);
    }
    return;
}

int main() {
    int m, t, x;
    cin >> m;
    while (m--) {
        cin >> t;
        if (t == 1) {
            cin >> x;
            val[++cnt] = x; 
            up(cnt);
        }
        else {
            cout << val[1] << endl;
            val[1] = val[cnt];
            val[cnt--] = 0;
            down(1);
        }
    }
    return 0;
}

建堆过程

优先级队列中还有一种构造函数,把一组数值直接构建成一个堆。

  • 方法一:在空堆中,执行N次连续插入
    • 每一次插入之后都要维护堆的有序性,会做很多额外的工作
    • 复杂度: 最坏是 O ( N l o g N ) O(NlogN) O(NlogN),平均是 O ( N ) O(N) O(N)
  • 方法二: 不考虑中间状态,保证所有元素加入后满足堆的特性
    • 做法: 利用堆的递归定义,将左子树和右子树调整成堆,对根结点调用向下过滤percolateDown,以恢复堆的有序性。
    • 复杂度: 最坏情况下是 O ( N ) 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();
} 
大根堆的前序遍历
现有一个空的大根堆,现依次插入n个元素,我们想知道最终该大根堆的前序遍历。

输入描述:
第一行一个整数n (1 ≤ n ≤ 10^5),表示依次插入元素的个数。
第二行n个整数,每个整数之间用空格隔开,表示插入的元素。
对于100%的数据满足1 ≤ n ≤ 10^5。

输出描述:
一行n个整数,每个整数之间用空格隔开,表示该大根堆的前序遍历。

示例 1:
输入:
3
1 2 3
输出:
3 1 2
#include <iostream>
#define N 100010

using namespace std; 

int val[N], n;

void up(int pos) {
    while (pos != 1) {
        int pre = pos / 2;
        if (val[pos] > val[pre]) {
            swap(val[pos], val[pre]);
            pos = pre;
        }
        else
            break;
    }
    return;
}

void getAns(int pos) {
    cout << val[pos] << " ";
    if (pos * 2 <= n)
        getAns(pos * 2);
    if (pos * 2 + 1 <= n)
        getAns(pos * 2 + 1);
    return;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> val[i];
        up(i);
    }
    getAns(1);
    return 0;
}

哈希表

什么是哈希表

哈希表指的是根据关键字(key)而直接进行访问的动态查找表。

例子1:处理的所有的数据元素的关键字都是较小的非负整数 ( 0  -  65535 ) (0 \text{ - } 65535) 0 - 65535的动态查找表。

  • 解决方案:
    • 用长度为 65536 65536 65536 的数组存储。
    • 初始化时,将一个下标为 0 0 0 65535 65535 65535 的数组a初始化为一个特殊的、表示单元为空的值
    • insert(i):将 i \text{i} i存放到a[i.key]
    • find(key):取出a[key]的值
    • remove(key):将a[key]重新设为空值

例子2:假设关键字是 32 32 32位的整数,而元素数量很少,如 1000 1000 1000个。

  • 具体分析:
    • 数组a大小是 40 40 40亿,一般程序不会支持这么大的数组
    • 其中只用了 1000 1000 1000个元素,空间效率太低
  • 解决方案:
    • 设计一个将大整数映射成小整数的函数,函数值作为存储元素的下标。
    • 这个函数称为哈希函数(hash function),也称为散列函数

例子3:关键字是非整数,不能用来作为数组的下标。

  • 解决方案:
    • 设计一个转换成整数的哈希函数。

实现哈希表待解决的问题:

  • 哈希函数的设计
  • 冲突的解决(不同的关键字映射到同一个函数子)

哈希函数

什么是哈希函数

  • 以结点的关键字值为参数
  • 函数值是元素的存储位置
  • 如果数组大小是 n n n,哈希函数的值域为0 ~ n-1

哈希函数的选择标准

  • 计算速度快
  • 散列地址尽可能均匀,使得冲突机会尽可能的少

常用的哈希函数:

  • 直接地址法
    H(key) = key 或 H(key) = a * key + b

    • 例子:
      如果关键字集合为 { 100 , 400 , 600 , 200 , 800 , 900 } \{100,400,600,200,800,900\} {100400600200800900}
      取散列函数为H(x)= key,需要 901 901 901个单元。取H(x) = x/100,只需要 10 10 10个单元。
  • 除留取余法
    H(key)= key MOD p p是数组的大小

    • 这样可以保证函数值是一个合法的下标。

    • p最好为质数,函数值分布更均匀。

      • key 值都为奇数,选 p 为偶数;则 H(key) = key MOD p ,结果为奇数,一半单元被浪费掉。

      • key 值都为 5 5 5 的倍数,选 p 95 95 95;则 H(key) = key MOD p ,结果为: 0 、 5 、 10 、 15 、 … … 90 0、 5、10、15、…… 90 05101590 4 / 5 4/5 4/5 的单元被浪费掉。

  • 数字分析法

对关键字集合中的所有关键字,分析每一位上数字分布;取数字分布均匀的位作为地址的组成部分

  • 平方取中法

将关键字平方后,取其结果的中间各位作为散列函数值

  • 适用于关键字中各位的分布都比较均匀,但关键字的值域比数组规模大

  • 由于中间各位和每一位数字都有关系,因此均匀分布的可能性较大

  • 例子:
    关键字是 4371 4371 4371,则计算 4731 ∗ 4731 = 22382361 4731 * 4731 = 22382361 47314731=22382361
    中间部分究竟要选取几位,依赖于散列表的单元总数。
    若散列表总共有 100 100 100个单元,可以选取最中间的部分,即第 4 、 5 4、5 45位,那么关键字值为 4731 4731 4731的结点的散列地址可选为 82 82 82

  • 折叠法

选取一个长度后,将关键字按此长度分组相加

  • 如果关键字相当长,以至于和散列表的单元总数相比大得多时,可采用此法。

  • 例子:
    关键字值为 542242241 542242241 542242241,按 3 3 3位折叠,可以得到 542 + 242 + 241 = 1025 542+242+241=1025 542+242+241=1025
    抛弃进位,得到散列结果为 25 25 25

老菲林的疑问
老菲林经营着一家公司,这家公司里每位员工都有一个编号,有些干员外出执行任务,现在并不在公司本部,
老菲林现在先要查询一些员工现在是否在公司本部,但是干员太多了,老菲林记不清楚,请你设计一个程序帮她查询一下。
为了保证查询的准确性,技术部的希尔可露告诉你,老菲林想要询问的干员编号,在模大质数19260817的剩余系下,是不会重复的。

输入描述:
第一行两个整数n m,分别表示当前在公司的干员数量和老菲林的查询次数。
第二行n (1 ≤ n ≤ 10^5)个整数,表示当前在公司的干员编号,每个整数间用空格分开。
第三行m (1 ≤ m ≤ 10^5)个整数,表示当前老菲林查询的干员编号,每个整数间用空格分开。

注意:干员的编号有可能超过int的存储范围。

输出描述:
m行,每行一个整数,对应查询的结果。
若该干员在本部,则输出yes;反之,则输出no。

示例 1:
输入:
3 2
1 2 3
1 10
输出:
yes
no
#include <iostream>

#define mo 19260817

using namespace std; 

long long vis[mo];

int main() {
    int n, m;
    long long t;
    cin >> n >> m;
    while (n--) {
        cin >> t;
        vis[t % mo] = t;
    }
    while (m--) {
        cin >> t;
        if (vis[t % mo] == t)
            cout << "yes" << endl;
        else
            cout << "no" << endl;
    }
    return 0;
}

线性探测法

线性探测法概念与设计
线性探测法是解决冲突最简单的方法。

  • 插入:当散列发生冲突时,探测下一个单元,直到发现一个空单元。

  • 查找:

计算 addr = H(key)
  从 addr 开始遍历数组
       if (找到)  返回 true
       if (内容为空)  返回 false 
       ++addr
返回 false
  • 删除:采用迟删除,找到该元素,做一个删除标记

基于线性探测法的哈希表类定义

  • 简单构造:

    • 数据成员:一个动态数组 + 指向函数的指针
    • 成员函数:插入、查找和删除以及构造析构 + 一个缺省的关键字转换函数
  • 进一步细化:

    • 数组元素类型:node类型,存储数据及存储单元的状态
    • 存储数据及存储单元的状态:将关键字转成整数的函数,应该由用户提供
    • 如果关键字本身就是整数,不需要用户提供这个函数:为指向函数的指针成员提供一个缺省值
  • 实现代码:

template <class KEY, class OTHER>
class closeHashTable: public dynamicSearchTable<KEY, OTHER> {
    private:
        struct node {              //散列表的结点类
            SET <KEY, OTHER> data;
            int state;             //0 -- empty   1 -- active   2 -- deleted
            node() {state = 0;}
        };
        node *array;
        int  size;
        int (*key)(const KEY &x);
        static int defaultKey(const int &x) {return x;} 
    public: 
        closeHashTable(int length = 101, int (*f)(const KEY &x) = defaultKey);
        ~closeHashTable() {delete [] array;}
        SET<KEY, OTHER> *find(const KEY &x) const;
        void insert(const SET<KEY, OTHER> &x);
        void remove(const KEY &x);
};

基于线性探测法的哈希表成员函数实现

  • 构造函数:构造一个空的散列表
template <class Type>
closeHashTable<Type>::closeHashTable (int length, int (*f)(const Type  &x)) {
    size = length;
    array = new node[size];
    key = f;
} 
  • insert函数:散列地址开始寻找一个空单元或被删单元,散列函数采用除留余数法
template <class KEY, class OTHER>
void closeHashTable<KEY, OTHER>::insert(const SET<KEY, OTHER> &x) {
    int initPos, pos ;

    initPos = pos = key(x.key) % size;
    do {  
        if (array[pos].state != 1) {       // 找到空单元
            array[pos].data = x; 
            array[pos].state = 1; 
            return;
        }
        pos = (pos+1) % size;
    } while (pos != initPos);
}
  • find函数:从散列地址开始寻找被查元素,直到找到该元素或空单元
template <class KEY, class OTHER>
SET<KEY, OTHER> *closeHashTable<KEY, OTHER>::find(const KEY &x) const {
    int initPos, pos;
    
    initPos = pos = key(x) % size;
    do {
        // 没有找到
        if (array[pos].state == 0) return NULL;
        // 找到
        if (array[pos].state == 1 && array[pos].data.key == x)
            return (SET<KEY,OTHER> *)&array[pos];
        pos = (pos+1) % size;
    } while (pos != initPos);
    return NULL;
}
  • remove函数;从散列地址开始寻找被删元素,找到后做删除标志
template <class KEY, class OTHER>
void closeHashTable<KEY, OTHER>::remove(const KEY &x) {
    int initPos, pos ;
  
    initPos = pos = key(x) % size;
    do {
        if (array[pos].state == 0) return;
        if (array[pos].state == 1 && array[pos].data.key == x) {     // 找到,删除
            array[pos].state = 2;
            return;
        }
        pos = (pos+1) % size;
    } while (pos != initPos);
}

线性探测法类的缺陷

  • 没有保证表长为素数
    • 可以在构造函数中增加素数检测。如果实际参数的表长不是素数,则将大于等于表长的最小素数作为表长。
  • 运行一段时间以后,所有数组元素都成为active或deleted,所有操作的时间性能都是O(N)
    • 可以增加一个整理散列表的功能,将被删元素真正删去
    • 可以增加一个数据成员记录deleted的单元数,自动调用整理函数
元素存在确认
已知一个长度为n (1 ≤ n ≤ 10^5)的数列,其中元素保证不重复。现在我们想查询一些数字是否在这个数列中出现。

输入描述:
第一行两个整数n m (1 ≤ m ≤ 10^5),分别表示数列的长度和查询次数。
第二行n个整数,表示该数列。
第三行m个整数,表示我们要查询的数字。
注意:数字的大小有可能超过int的存储范围。

输出描述:
m行,每行一个整数,对应查询的结果。

若该次查询的数字在数列中,则输出yes;反之,则输出no。

示例 1:
输入:
3 2
1 2 3
1 10
输出:
yes
no
#include <bits/stdc++.h>

#define mo 19260817

using namespace std; 

long long vis[mo];

int main() {
    int n, m, pos;
    long long t;
    cin >> n >> m;
    while (n--) {
        cin >> t;
        pos = t % mo;
        while (vis[pos])
            pos++;
        vis[pos] = t;
    }
    while (m--) {
        cin >> t;
        pos = t % mo;
        while (vis[pos] != 0 && vis[pos] != t)
            pos++;
        if (vis[t % mo] == t)
            cout << "yes" << endl;
        else
            cout << "no" << endl;
    }
    return 0;
}

图的定义

图的概念
图是四种逻辑结构中最复杂的结构:

  • 集合结构:其中的元素之间没有关系
  • 线性结构:严格的一对一关系
  • 树状结构:一对多的关系
  • 图状结构:多对多关系

在数学中,图可以用 G = ( V , E ) G=(V, E) G=(V,E) 来定义,其中, V V V 表示图中的顶点集, E E E 表示图中的边集。

  • 有向图:

    • 边有方向,也称为弧
    • 边用 < > <> <>表示,比如, < A , B > <A,B> <A,B> 表示从 A A A 出发到 B B B 的一条边
      在这里插入图片描述
  • 无向图:

    • 边无方向
    • 边用圆括号表示, ( A , B ) (A,B) (A,B) 表示顶点 A A A B B B 之间有一条边
      在这里插入图片描述
  • 加权图:

    • 边被赋予一个权值 W W W
    • 如果图是有向的,称为加权有向图,边表示为 < A , B , W > <A, B, W> <A,B,W>
    • 如果是无向的,称为加权无向图,边表示为 ( A , B , W ) (A, B, W) (A,B,W)
      在这里插入图片描述

图的操作

  • 基本操作
    • 构造一个由若干个结点、0 条边组成的图
    • 判断两个结点之间是否有边存在
    • 在图中添加或删除一条边
    • 返回图中的结点数或边数
    • 按某种规则遍历图中的所有结点
  • 还有一些与应用密切关联的操作
    • 拓扑排序
    • 关键路径
    • 找最小生成树
    • 找最短路径等

图的抽象类

template <class TypeOfVer, class TypeOfEdge>
class graph {
    public:
        virtual void insert(TypeOfVer x, TypeOfVer y, TypeOfEdge w) = 0;
        virtual void remove(TypeOfVer x, TypeOfVer y) = 0;
        virtual bool exist(TypeOfVer x, TypeOfVer y) const = 0;
        virtual  ~graph() {}
        int numOfVer() const {return Vers;}
        int numOfEdge() const {return Edges;}

    protected:
        int Vers, Edges;
};

图的术语

图中连接于某一结点的边的总数

  • 入度:有向图中进入某一结点的边数
  • 出度:有向图中离开某一结点的边数

子图

设有两个图 G = ( V , E ) G = (V, E) G=(V,E) G ′ = ( V ’ , E ’ ) G' = (V’, E’) G=(V,E),如果 V ′ ⊂ V , E ′ ⊂ E V'\subset V, E'\subset E VV,EE,则称 G ’ G’ G G G G 的子图
在这里插入图片描述

路径

1 < i < N 1<i<N 1<i<N,结点序列 w 1 , w 2 , . . . , w N w_1,w_2,...,w_N w1,w2,...,wN中的结点对 ( w i , w i + 1 ) (w_i, w_{i+1}) (wi,wi+1) 都有 ( w i , w i + 1 ) ∈ E (w_i, w_{i+1})\in E (wi,wi+1)E < w i , w i + 1 > ∈ E <w_i, w_{i+1}> \in E <wi,wi+1>E

  • 简单路径:如果一条路径上的所有结点,除了起始结点和终止结点可能相同外,其余的结点都不相同,比如下图中的 BACD、ABA
  • 环:环是一条简单路径,其起始结点和终止结点相同,且路径长度至少为 1 1 1,比如下图中的 ACDA、ABA
    在这里插入图片描述

路径长度

  • 非加权的路径长度:组成路径的边数
  • 加权路径长度:路径上所有边的权值之和

连通性

  • 无向图:

    • 连通:顶点 v v v v ’ v’ v 之间有路径存在
    • 连通图:任意两点之间都是连通的无向图,比如下面的左图就是一个连通图,右图就不是
    • 连通分量:非连通图中的极大连通子图,比如下面的右图就存在红圈圈住的两个连通分量
      在这里插入图片描述
  • 有向图:

    • 强连通图:顶点 v v v v ’ v’ v 之间有路径存在
    • 强连通分量:非强连通图中的极大连通子图,比如下面的左图就是一个强连通图,右图就不是,右图存在红圈圈住的三个强连通分量
    • 弱连通图:如有向图 G G G 不是强连通的,但如果把它看成是无向图时是连通的,比如下面的右图
      在这里插入图片描述

完全图

  • 无向完全图:任意两个结点之间都有边的无向图,比如下面的左图
  • 有向完全图:任意两个结点之间都有弧的有向图,比如下面的右图
    在这里插入图片描述

生成树

连通图的极小连通子图,如下图中右侧两图就是左图的生成树

  • 包含图的所有 n n n 个结点和 n − 1 n−1 n1 条边
  • 在生成树中添加一条边之后,必定会形成回路或环
    在这里插入图片描述

最小生成树

加权无向图的所有生成树中边的权值(代价)之和最小的生成树,如下图中右图就是左图的最小生成树
在这里插入图片描述

图的存储

邻接矩阵
邻接矩阵的概念和设计

  • 有向图

    • V V V 集合存储在一个数组中
    • E E E 集合用 n n n n n n 列的布尔矩阵 A A A 表示
      • 如果 i i i j j j 有一条有向边, A [ i , j ] = 1 A[i,j] = 1 A[i,j]=1
      • 如果 i i i j j j 没有一条有向边, A [ i , j ] = 0 A[i,j] = 0 A[i,j]=0
    • 特点:
      • i i i个结点的出度: i i i 行之和
      • j j j个结点的入度: j j j 列之和
        在这里插入图片描述
  • 无向图

    • V 集合存储在一个数组中
    • E E E 集合用 n n n n n n 列的布尔矩阵 A A A 表示
      • 如果 i i i j j j 有一条边, A [ i , j ] = A [ j , i ] = 1 A[i,j] = A[j,i] = 1 A[i,j]=A[j,i]=1
      • 如果 i i i j j j 没有边, A [ i , j ] = A [ j , i ] = 0 A[i,j] = A[j,i] = 0 A[i,j]=A[j,i]=0
    • 特点:
      • 是一个对称矩阵
      • 结点 i i i 的度是 i i i 行或 i i i 列之和
        在这里插入图片描述
  • 加权图

    • 如果 i i i j j j 有一条边且它的权值为 a a a,则 A [ i , j ] = a A[i,j] = a A[i,j]=a
    • 如果 i i i j j j 没有一条有向边,则 A [ i , j ] = A[i,j] = A[i,j]= 空或其它标志
      在这里插入图片描述
  • 性能分析

    • 优点:基本操作都是 O ( 1 ) O(1) O(1) 的时间复杂度,不仅能找到出发的边,也能找到到达的边
    • 缺点:即使 < < n 2 << n^2 <<n2 条边,也需内存 n 2 n^2 n2个单元,而大多数的图的边数远远小于 n 2 n^2 n2

邻接矩阵类定义

template <class TypeOfVer, class TypeOfEdge>
class adjMatrixGraph::public graph<TypeOfVer, TypeOfEdge> {
public:
    adjMatrixGraph(int vSize, const TypeOfVer d[], const TypeOfEdge noEdgeFlag);
    void insert(TypeOfVer x, TypeOfVer y, TypeOfEdge w);
    void remove(TypeOfVer x, TypeOfVer y);
    bool exist(TypeOfVer x, TypeOfVer y) const;
    ~adjMatrixGraph()private:
    TypeOfEdge **edge;                   //存放邻接矩阵
    TypeOfVer *ver;                      //存放结点值
    TypeOfEdge noEdge;                   //邻接矩阵中的∞的表示值
    int find(TypeOfVer v) const {
        for (int i = 0; i < Vers; ++i)
            if (ver[i] == v) return i;
    }
};

邻接矩阵成员函数实现

  • 构造函数
template <class TypeOfVer, class TypeOfEdge>
adjMatrixGraph<TypeOfVer, TypeOfEdge>::adjMatrixGraph (int vSize, const TypeOfVer d[], TypeOfEdge noEdgeFlag) {
    int i, j;
    // 结点数和边数存储到父类的成员变量中
    Vers = vSize;   
    Edges = 0;  
    noEdge = noEdgeFlag;

    ver = new TypeOfVer[vSize];
    for (i=0; i<vSize; ++i) ver[i] = d[i];  
 
    edge = new TypeOfEdge*[vSize];
    for (i=0; i<vSize; ++i) {
        edge[i] = new TypeOfEdge[vSize];
        for (j=0; j<vSize; ++j) edge[i][j] = noEdge;
    }
}
  • 析构函数
template <class TypeOfVer, class TypeOfEdge>
adjMatrixGraph<TypeOfVer, TypeOfEdge>::~adjMatrixGraph() {
    delete [] ver;
    for (int i=0; i<Vers; ++i) 
        delete [] edge[i];
    delete [] edge;
}
  • insert函数
template <class TypeOfVer, class TypeOfEdge>
void adjMatrixGraph<TypeOfVer, TypeOfEdge> ::insert(TypeOfVer x, TypeOfVer y, TypeOfEdge w) { 
    int u = find(x), v = find(y); 
    edge[u][v] = w;
    ++Edges;
}
  • remove函数
template <class TypeOfVer, class TypeOfEdge>
void adjMatrixGraph<TypeOfVer, TypeOfEdge>::remove(TypeOfVer x, TypeOfVer y) { 
    int u = find(x),  v = find(y); 
    edge[u][v] = noEdge;
    --Edges;
}

邻接表
邻接表的概念和设计

  • 邻接表是图的标准存储方式

    • V V V 集合
      • 用数组或单链表的形式存放所有的结点值
      • 如果结点数 n n n 固定,则采用数组形式,否则可采用单链表的形式
    • E E E 集合
      • 同一个结点出发的所有边组成一个单链表
      • 注意:如果是加权图,单链表的每个结点中还要保存权值
        在这里插入图片描述
  • 性能分析

    • 优点:
      内存 = 结点数 + 边数
      处理时间:结点数 + 边数,即为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
    • 缺点:
      • 确定 i − > j i->j i>j 是否有边,最坏需耗费 O ( n ) O(n) O(n) 时间
      • 无向图同一条边表示两次,边表空间浪费一倍
      • 有向图中寻找进入某结点的边,非常困难

邻接表类定义

template <class TypeOfVer, class TypeOfEdge>
class adjListGraph::public graph<TypeOfVer, TypeOfEdge> {
public:      
    adjListGraph(int vSize, const TypeOfVer d[]);
    void insert(TypeOfVer x, TypeOfVer y, TypeOfEdge w);
    void remove(TypeOfVer x, TypeOfVer y);
    bool exist(TypeOfVer x, TypeOfVer y) const;
    ~adjListGraph();
    
private:    
    struct edgeNode {                   
        int end;                          
        TypeOfEdge weight;                
        edgeNode *next;
        edgeNode(int e, TypeOfEdge w, edgeNode *n = NULL) { end = e; weight = w; next = n;}
    };

    struct verNode{                     
        TypeOfVer ver;                   
        edgeNode *head;                 
        verNode( edgeNode *h = NULL) { head = h;}
    };

    verNode *verList;
    int find(TypeOfVer v) const { 
        for (int i = 0; i < Vers; ++i)
            if (verList[i].ver == v) return i; 
    } 
};

邻接表成员函数实现

  • 构造函数:假设所有单链表用的都是不带头结点的单链表,那我们需要构造一个数组存放顶点,每个顶点中的edgeNode都是空的
template <class TypeOfVer, class TypeOfEdge>
adjListGraph<TypeOfVer, TypeOfEdge> ::adjListGraph(int vSize, const TypeOfVer d[]) {
    Vers = vSize; 
    Edges = 0;
 
    verList = new verNode[vSize];
    for (int i = 0; i < Vers; ++i) 
		verList[i].ver = d[i];
}
  • 析构函数
template <class TypeOfVer, class TypeOfEdge>
adjListGraph<TypeOfVer, TypeOfEdge>::~adjListGraph() { 
	int i;
    edgeNode *p;
    
    for (i = 0; i < Vers; ++i) 
		while ((p = verList[i].head) != NULL) {
			verList[i].head = p->next;
	    	delete p;
		}

	delete [] verList;
} 
  • insert函数
template <class TypeOfVer, class TypeOfEdge>
void adjListGraph<TypeOfVer, TypeOfEdge>:: insert(TypeOfVer x, TypeOfVer y, TypeOfEdge w) {
	int u = find(x),  v = find(y);
	verList[u].head = new edgeNode(v, w, verList[u].head);
    ++Edges;
}
  • remove函数
template <class TypeOfVer, class TypeOfEdge>
void adjListGraph<TypeOfVer,TypeOfEdge>::remove(TypeOfVer x,TypeOfVer y) {  
	int u = find(x), v = find(y);
	edgeNode *p = verList[u].head, *q; 

	if (p == NULL) return;  
	if (p->end == v) {       
		verList[u].head = p->next; 
		delete p;
		--Edges;
		return;
	} 
	while (p->next !=NULL && p->next->end != v) p = p->next;        
	if (p->next != NULL) {               
		q = p->next;        
		p->next = q->next;         
		delete q;       
		--Edges;  
	}
}
  • exist函数
template <class TypeOfVer, class TypeOfEdge>
bool adjListGraph<TypeOfVer, TypeOfEdge>::exist(TypeOfVer x, TypeOfVer y) const {
	int u = find(x),  v = find(y);
    edgeNode *p = verList[u].head;
 
    while (p !=NULL && p->end != v) 
    	p = p->next;
	if (p == NULL) 
    	return false; 
	else return true;
}

逆邻接表
将进入同一结点的边组织成一个单链表。
在这里插入图片描述

十字链表
每个结点维护两条链,既记录前驱又记录后继,而且每条边只存储一次。
在这里插入图片描述

邻接多重表
解决无向图中边存储两次的问题。

每个边的链表结点中存储与这条边相关的两个顶点,以及分别依附于这两个顶点下一条的边。
在这里插入图片描述

连通路径
已知一张图有n (2 <= n <= 1000)个点,m (1 <= m <= 10^6)条边,点的编号为1 ~ n,现在给出每条边连接的两侧端点,请你列出每个点都与哪些点有直接连通的边。

输入描述:
第一行两个整数n m,分别表示这张图点和边的数量。
接下来m行,每行两个整数,表示这条边所连接的两个端点。

输出描述:
共n行,每行若干个整数,第i行表示与点i相直接连接的点。若有多个,则由小到大输出,不包含点i自己;若没有点与其直接相连,则在改行输出none。

示例 1:
输入:
5 5
1 2
2 3
1 3
2 1
1 4
输出:
2 3 4
1 3
1 2
1
none
#include <iostream>

#define N 1010

using namespace std; 

bool nxt[N][N];

int main() {
    int n, m, a, b;
    cin >> n >> m;
    while (m--) {
        cin >> a >> b;
        nxt[a][b] = true;
        nxt[b][a] = true;
    }
    for (int i = 1; i <= n; i++) {
        bool f = false;
        for (int j = 1; j <= n; j++)
            if (nxt[i][j] == true && i != j) {
                cout << j << " ";
                f = true;
            }
        if (f == false)
            cout << "none";
        cout << endl;
    }
    return 0;
}

图的遍历

深度优先遍历

深度优先遍历的概念

  • 过程:
    1.选中第一个被访问的顶点
    2.对顶点做已访问过的标志
    3.依次从顶点的未被访问过的第1个、第2个、第3个、……邻接顶点出发,进行深度优先搜索
    4.如果还有顶点未被访问,则选中一个起始顶点,转向2
    5.所有的顶点都被访问到,则结束
  • 举例:
    • 从结点5开始进行深度优先的搜索,则遍历序列
      • 可以为:5,7,6,2,4,3,1,
      • 也可以为:5,6,2,3,1,4,7。
        在这里插入图片描述

深度优先遍历的实现

  • 实现方法:
    • 在图类中加一个深度优先遍历的函数dfs
    • 函数原型:定义访问是输出元素值,void dfs ();
    • 函数实现:采用递归实现,私有成员函数的参数是从哪个结点开始遍历
    • 递归函数实现:访问参数结点。对每一个后继结点,如果没有访问过,对该结点递归调用本函数
  • 问题:
    • 图可能有回路:必须对访问过的顶点加以标记,设置一个数组visited,记录结点是否被访问过
    • 图不一定连通:从某个结点开始dfs不一定遍历到所有结点,私有的dfs调用结束后,必须检查是否有结点没有被遍历,从该结点出发开始一个dfs
template <class TypeOfVer, class TypeOfEdge>
void adjListGraph<TypeOfVer, TypeOfEdge>::dfs() const {
    bool *visited = new bool[Vers]; 

    for (int i=0; i < Vers; ++i) 
        visited[i] = false;

    cout << "当前图的深度优先遍历序列为:" << endl;
    for (i = 0; i < Vers; ++i) {
        if (visited[i] == true)
            continue;
        dfs(i, visited);
        cout << endl;
    }
}

template <class TypeOfVer, class TypeOfEdge>
void adjListGraph<TypeOfVer, TypeOfEdge>::dfs (int start, bool visited[]) const {
    edgeNode *p = verList[start].head;
  
    cout << verList[start].ver << '\t';  
    visited[start] = true;
    while (p != NULL)  { 
        if (visited[p->end] == false) 
            dfs(p->end, visited);
        p = p->next;
    }
} 

深度优先遍历的时间性能分析
dfs函数将对所有的顶点和边进行访问

  • 如果图用邻接表存储:访问了每个结点及结点的单链表 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 如果图用邻接矩阵存储:访问某个结点出发的边需要遍历邻接矩阵中对应的行 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)
图的深度优先遍历
已知一张图有n个点,点的编号为1 ~ n,
现在给出该图的邻接矩阵,请你以节点x为第一次的起点,进行深度优先遍历,并输出节点访问的顺序。
对于任一起点,若有多个出路,则首先遍历的是序号最小的点。并保证最后的遍历顺序的字典序最小。

对于全部测试数据满足n ≤ 100。

输入描述:
第一行两个整数n x,n表示图的顶点数,x表示遍历的开始顶点;
接下来n行,每行n个整数,表示该图的邻接矩阵,若为0表示没有边,为1则表示有边。

输出描述:
一行,以顶点x为起点的深度优先遍历序列,每个数字用空格隔开。

示例 1:
输入:
4 1
0 1 0 1
1 0 1 1
0 1 0 1
1 1 1 0
输出:
1 2 3 4
#include <iostream>

#define N 110

using namespace std; 

int nxt[N][N], n, x;
bool vis[N];

void dfs(int pos) {
    cout << pos << " ";
    vis[pos] = true;
    for (int i = 1; i <= n; i++)
        if (nxt[pos][i] == 1 && vis[i] == false)
            dfs (i);
    return;
}

int main() {
    cin >> n >> x;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cin >> nxt[i][j];
    dfs(x);
    for (int i = 1; i <= n; i++)
        if (vis[i] == false)
            dfs(i);
    return 0;
}

广度优先遍历

广度优先遍历的概念

  • 过程
    1.选中第一个被访问的顶点
    2.对顶点做已访问过的标志
    3.依次访问已访问顶点的未被访问过的第1个、第2个、第3个、……、第 m m m个邻接顶点 W 1 、 W 2 、 W 3 、 … … 、 W m W_1、W_2、W_3、……、W_m W1W2W3Wm,进行标记,转向3
    4.如果还有顶点未被访问,则选中一个起始顶点,转向2
    5.所有的顶点都被访问到,则结束
  • 举例
    • 从结点5开始广度优先搜索这个图,则遍历序列为:5,6,7,2,4,3,1
    • 如果按照顶点序号小的先访问,序号大的后访问的原则,则它的广度优先访问序列为:1,2,4,3,5,6,7
      在这里插入图片描述

广度优先遍历的实现

  • 与树的层次遍历类似,需要一个队列来记录哪些结点可以被访问
  • 从序号最小的结点开始进行bfs
  • 过程如下:
    • 将序号最小的顶点放入队列
    • 重复取队列的队头元素,直到队列为空
      • 对出队的每个元素,首先检查该元素是否已被访问。如果没有被访问过,则访问该元素,并将它的所有的没有被访问过的后继入队
    • 检查是否还有结点未被访问。如果有,重复上述两个步骤
template <class TypeOfVer, class TypeOfEdge>
void adjListGraph<TypeOfVer, TypeOfEdge>::bfs() const {
    bool *visited = new bool[Vers];
    int currentNode;
    linkQueue<int> q;
    edgeNode *p; 
    for (int i=0; i < Vers; ++i) 
        visited[i] = false; 
    cout << "当前图的广度优先遍历序列为:" << endl; 
    for (i = 0; i < Vers; ++i) {
        if (visited[i] == true) continue;
        q.enQueue(i);
        while (!q.isEmpty()) {
            currentNode = q.deQueue();
            if (visited[currentNode] == true) continue;
            cout << verList[currentNode].ver << '\t';
            visited[currentNode] = true;
            p = verList[currentNode].head;
            while (p != NULL){
                if (visited[p->end] == false) q.enQueue(p->end);
                p = p->next;
            }
        }
        cout << endl;
    }
} 

广度优先遍历的时间性能分析
bfs函数将对所有的顶点和边进行访问

  • 如果图用邻接表存储:访问了每个结点及结点的单链表 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  • 如果图用邻接矩阵存储:访问某个结点出发的边需要遍历邻接矩阵中对应的行 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)
奇怪的电梯
有一栋大楼总共有N (1 ≤ N ≤ 200)层。大楼的每一层楼都可以停电梯,而且第i (1 <= i <= N)层楼上有一个数字Ki (0 <= Ki <= N)。

电梯只有四个按钮:开,关,上,下。上下的层数等于当前楼层上的那个数字。

当然,如果不能满足要求,相应的按钮就会失灵。

例如:3 3 1 2 5代表了Ki ( K1=3, K2=3, …… ),从一楼开始。在一楼,按上可以到4楼,按下是不起作用的,因为没有-2楼。

那么,从A楼到B楼至少要按几次按钮呢?

输入描述:
第一行为三个用空格隔开的正整数,表示N A B。全部数据保证1 ≤ A,B ≤ N。
第二行为N个用空格隔开的正整数,表示Ki。

输出描述:
一行,即最少按键次数,若无法到达,则输出-1。

示例 1:
输入:
5 1 5
3 3 1 2 5
输出:
3
#include <iostream>
#include <cstring>
#include <queue>

using namespace std; 

int mov[210], stp[210];
queue<int> que;

int main() {
    int n, a, b;
    cin >> n >> a >> b;
    for (int i = 1; i <= n; i++)
        cin >> mov[i];
    memset(stp, -1, sizeof(stp));
    stp[a] = 0;
    que.push(a);
    while (!que.empty()) {
        int pos = que.front();
        que.pop();
        if (pos - mov[pos] >= 1 && stp[pos - mov[pos]] == -1) {
            stp[pos - mov[pos]] = stp[pos] + 1;
            que.push(pos - mov[pos]);
        }
        if (pos + mov[pos] <= n && stp[pos + mov[pos]] == -1) {
            stp[pos + mov[pos]] = stp[pos] + 1;
            que.push(pos + mov[pos]);
        }
    }
    cout << stp[b];
    return 0;
}

欧拉回路

欧拉回路的问题
欧拉回路是图论中的经典问题之一,源于哥尼斯堡七桥问题。

哥尼斯堡七桥问题:

下面左图是哥尼斯堡的地形,右图是该问题抽象出的图。能否走遍七座桥,且每座桥只经过一次,最后又回到出发点?
在这里插入图片描述

欧拉的结论:

  • 如果有奇数桥的地方不止两个,不存在的路径;
  • 如果只有两个地方有奇数桥,可以从这两个地方之一出发,经过所有的桥一次,再回到另一个地方,称欧拉路径
  • 如果都是偶数桥,从任意地方出发都能回到原点,称欧拉回路

欧拉回路的问题:给定一个有欧拉回路的图,如何找到一条欧拉回路?

欧拉回路的解决方法

  • 检查存在性

    • 检查每个结点的度数是否为偶数
  • 找出回路

    • 执行一次深度优先的搜索。从起始结点开始,沿着这条路一直往下走,直到无路可走。在此过程中不允许回溯。
    • 路径上是否有一个尚有未访问的边的顶点。如果有,开始另一次深度优先的搜索,将得到的遍历序列拼接到原来的序列中,直到所有的边都已被访问。
  • 举例:如下图

    • 先找到 5->4->3->5
    • 在路径上找一个尚有边未被访问的结点,如:4,开始另一次深度优先遍历。得到路径4->2->1->4
    • 将第二条路径拼接到第一条路径上,得到:5->4->2->1->4->3->5
    • 3号结点还有未访问的边,从3号结点再开始一次深度优先遍历,得到路径3->1->0->2->3
    • 将第三条路径拼接到第一条路径上,得到:5->4->2->1->4->3->1->0->2->3->5
      在这里插入图片描述

欧拉回路的实现

  • 在邻接表类中增加一个公有成员函数EulerCircuit
    • 调用私有的EulerCircuit函数获得一段段的回路,并将它们拼接起来,形成一条完整的欧拉回路
  • 私有的成员函数EulerCircuit:获得一段回路
  • 如何存储欧拉回路?
    • 单链表:拼接方便
  • 如何保证每条路只能走一遍?
    • clone函数创建一份邻接表的拷贝,以便在找完路径后能恢复这个图的邻接表
    • 当一条边被访问以后,就将这条边删除

单链表中欧拉结点的实现

struct EulerNode {
    int NodeNum;
    EulerNode *next;
    EulerNode(int ver) {
        NodeNum = ver; 
        next =NULL;
    }
}; 

公有EulerCircuit的实现

template <class TypeOfVer, class TypeOfEdge>
void adjListGraph<TypeOfVer, TypeOfEdge>::EulerCircuit(TypeOfVer start) { 
    EulerNode *beg, *end, *p, *q, *tb, *te;
    int numOfDegree;
    edgeNode  *r;
    verNode *tmp;       
  
    //检查是否存在欧拉回路
    for (int i=0; i<Vers; ++i) {
        numOfDegree = 0;  r = verList[i].head;
        while (r != 0) {
            ++numOfDegree;
            r = r->next;
        }
        if (numOfDegree == 0 || numOfDegree % 2) {
            cout << "不存在欧拉回路" << endl;
            return;
        }
    }
    i = find(start);                    //寻找起始结点的编号
    tmp = clone();                      //创建一份邻接表的拷贝

    //寻找从i出发的路径,路径的起点和终点地址分别是beg和end
    EulerCircuit(i, beg, end);
 
    while (true) { 
        p = beg;
        while (p->next != NULL)             //检查p的后继结点是否有边尚未被访问
        if (verList[p->next->NodeNum].head != NULL)  break;    
        else p = p->next;
        if (p->next == NULL) break;                     //所有的边都已被访问
        q = p->next;                                    //尚有未被访问边的结点
        EulerCircuit(q->NodeNum, tb, te);               //从此结点开始找一段回路
        te->next = q->next;                             //将搜索到的路径拼接到原来的路径上
        p->next = tb;
        delete q;
    }
}

clone的实现

template <class TypeOfVer, class TypeOfEdge>
adjListGraph<TypeOfVer, TypeOfEdge>::verNode *adjListGraph<TypeOfVer, TypeOfEdge>::clone( ) const {
    verNode *tmp =  new verNode[Vers];
    edgeNode *p;
 
    for (int i = 0; i < Vers; ++i) { 
        tmp[i].ver = verList[i].ver; 
        p = verList[i].head;
        while (p != NULL) {  
            tmp[i].head =  new edgeNode(p->end, p->weight, tmp[i].head);
            p = p->next;
        }
    }
    return tmp;
} 

私有EulerCircuit的实现

template <class TypeOfVer, class TypeOfEdge>
void adjListGraph<TypeOfVer, TypeOfEdge>::EulerCircuit(int start, EulerNode *&beg, EulerNode *&end) {
    int nextNode;
 
    beg = end = new EulerNode(start);         //将起始结点放入欧拉回路
    while(verList[start].head != NULL) {      //起始结点尚有边未被访问
        nextNode = verList[start].head->end;
        remove( start,nextNode);   
        remove(nextNode, start);     
        start = nextNode;
        end->next = new EulerNode(start);
        end = end->next;
    }
}
欧拉路
已知一张无向图,请判断其中是否存在欧拉回路。

输入描述:
若干组测试数据。
每组第一行两个整数n m (1 ≤ n ≤ 100 1 ≤ m ≤ n * n),分别表示这张图点和边的数量。
接下来m行,每行两个整数,表示这条边所连接的两个端点。
当n为0时,表示输入结束。

输出描述:
一行,为欧拉路或欧拉回路的节点访问顺序。

示例 1:
输入:
3 3
2 3
1 2
1 3
3 2
1 2
2 3
0
输出:
1
0
#include <iostream>
#include <cstring>

#define N 110

using namespace std; 

bool vis[N], nxt[N][N];
int n, m, a, b, du[N];

void dfs(int pos) {
    vis[pos] = true;
    for (int i = 1; i <= n; i++)
        if (nxt[pos][i] == true && vis[i] == false)
            dfs(i);
    return;
}

int main() {
    while (1) {
        cin >> n;
        if (n == 0)
            break;
        cin >> m;
        memset(du, 0, sizeof(du));
        memset(nxt, false, sizeof(nxt));
        memset(vis, false, sizeof(vis));
        while (m--) {
            cin >> a >> b;
            nxt[a][b] = true;
            nxt[b][a] = true;
            du[a]++;
            du[b]++;
        }
        dfs(1);
        bool flag = true;
        for (int i = 1; i <= n; i++) {
            flag &= vis[i];
            if (du[i] % 2 == 1)
                flag = false;
        }
        cout << flag << endl;
    }
    return 0;
}

学习资源

青舟智学:https://www.qingzhouzhixue.com/

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝净云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值