【NOIp复习】数据结构之栈、队列和二叉树

1、STL中的stack头文件自带函数

  • empty()堆栈是否为空
  • push()压入元素
  • pop()弹出元素(并不会返回顶部元素,pop之前先判断!empty())
  • size()(返回栈的元素个数)
  • top()(返回栈顶元素)
  • 声明:stack<元素类型> 堆栈名
  • 复制:stack c1(c2) 代表将c2复制到c1

2、数制转换

输入格式

输入一个十进制数N与需要转换的进制d

输出格式

输出转换后的d进制数

思路分析

转换进制其实就是用短除法不停地除以d求余数,直到剩下的N比d小为止,倒着把余数从左到右写一遍的过程。因为这个“倒着取余数”的操作,让我们想到可以将计算过程在栈中顺序进行,然后弹出顺序就是倒序。

代码实现

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <stack>
using namespace std;

char ch[]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'};
int n,d;
stack<int> s;

int main(){
    scanf("%d%d",&n,&d);
    while(n){
        s.push(n%d);
        n=n/d;
    }
    while(!s.empty()) {
        printf("%c",ch[s.top()]); s.pop();
    }
    return 0;
} 

回顾反思

top并不会弹出栈顶元素,pop并不会返回栈顶元素,所以经常top过后就pop,并且要pop或者top之前务必确认!empty()

3、后序表达式

输入格式

一个带括号的四则运算表达式

输出格式

表达式的计算结果

思路分析

建两个栈,一个数字栈,一个符号栈。
从左到右读,遇到符号时先判断与栈顶符号的优先级顺序,如果入栈元素比栈顶元素优先级低,就弹出数字栈栈顶元素与符号栈栈顶符号计算,直到入栈元素优先级不低于栈顶元素为止。
原理就是保证符号栈中优先级始终从高到低,优先级高的永远比优先级低的先算。
处理括号:左括号优先级最低,右括号优先级最高,遇到右括号就一直弹出计算直到弹出第一个左括号为止。

队列

1、STL中的queue

  • queue q
  • front()返回队首元素
  • back()返回队尾元素
  • push()插入队尾
  • pop()队首出队
  • empty()队列是否为空
  • size()返回队列元素个数

2、(NOIP2015)机器翻译

题目描述

小晨的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章。
这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义来替换。对于每个英文单词,软件会先在内存中查找这个单词的中文含义,如果内存中有,软件就会用它进行翻译;如果内存中没有,软件就会在外存中的词典内查找,查出单词的中文含义然后翻译,并将这个单词和译义放入内存,以备后续的查找和翻译。
假设内存中有M个单元,每单元能存放一个单词和译义。每当软件将一个新单词存入内存前,如果当前内存中已存入的单词数不超过M-1,软件会将新单词存入一个未使用的内存单元;若内存中已存入M个单词,软件会清空最早进入内存的那个单词,腾出单元来,存放新单词。
假设一篇英语文章的长度为N个单词。给定这篇待译文章,翻译软件需要去外存查找多少次词典?假设在翻译开始前,内存中没有任何单词。

输入格式

输入文件共2行。每行中两个数之间用一个空格隔开。
第一行为两个正整数M和N,代表内存容量和文章的长度。
第二行为N个非负整数,按照文章的顺序,每个数(大小不超过1000)代表一个英文单词。文章中两个单词是同一个单词,当且仅当它们对应的非负整数相同。

输出格式

包含一个整数,为软件需要查词典的次数。

二叉树

二叉树数组表示法

对于一颗满二叉树(除了最底层节点以外,每个节点都有左右儿子),容易得到左儿子编号是父亲编号的2倍,右儿子为2n+1,所以把任意二叉树补成一颗满二叉树(没有的点就赋初值)即可。

二叉树链表表示法

struct tree{
    struct tree *left;//递归定义左右子树
    int data;//节点内存放的数据
    struct tree *right;
};
typedef struct tree treenode;//typedef A B将A类型的名字定义为B
typedef struct tree *b_tree;

b_tree insert(b_tree root, int node){//插入新节点
    b_tree newnode;//待插入节点
    b_tree currentnode;
    b_tree parentnode;

    newnode=(b_tree)malloc(sizeof(treenode));//分配空间
    newnode->data=node;
    newnode->left=NULL;
    newnode->right=NULL;

    if(root==NULL)//建立第一个节点
    return newnode;
    else{
        currentnode=root;//当前结点=当前根节点
        while(currentnode!=NULL)//找到有空儿子的节点为止,作为待插入节点的父节点
        {
            parentnode=currentnode;//父节点=当前结点
            if(currentnode->data>node)
                currentnode=currentnode->left;//如果父节点的数据比待插入节点大,当前结点=父节点的左节点
            else
                currentnode=currentnode->right;//如果父节点的数据比待插入节点小,当前结点=父节点的右节点
        }
        if(parentnode->data>node)//如果父节点比子节点大,子节点就到左子树,否则到右子树
            parentnode->left=newnode;
        else
            parentnode->right=newnode;
    }
    return root;
}

b_tree create(int *data, int len){//建立二叉树,data数组为需要建树的数据,len为数组长度,返回该二叉树的根节点地址;
    b_tree root=NULL;//根节点初始化为空
    for(int i=1;i<=len;i++)
        root=insert(root,data[i]);
    return root;
}

void print(b_tree root){//前序遍历输出二叉树
    if(root!=NULL){
        printf("%d ",root->data);
        print(root->left);
        print(root->right);
    }
}

二叉树的序遍历

前序遍历:根子树——左节点——右子树
中序遍历:左节点——根子树——右子树
后序遍历:左子树——右子树——根节点
下面是三种知二求一的算法…

求后序

前序遍历中第一个字母就是根节点,在中序遍历中找到这个字母,左边就是左子树的中序遍历,右边就是右子树的中序遍历;前序遍历紧接着的是左子树的前序遍历,然后是右子树的前序遍历,所以又变成了左右子树分别已知前序中序遍历求后序遍历的问题——递归。

举个栗子:已知
前序遍历为ABDEFGCH
中序遍历为DFEGBAHC
A把中序遍历分为了
左子树中序:DFEGB
右子树中序:HC
左子树前序:BDEFG
右子树前序:CH

求前序

和求后序的思路一毛一样

#include <cstdio>
#include <cstring>
#define RES(a,b) memset(a,b,sizeof(a))
using namespace std;

char in[20],post[20];
int len;

void print_pre(int inl, int inr, int pol, int por){
    if(inl>inr||pol>por) return;
    printf("%c",post[por]);
    int pos;
    for(int i=inl;i<=inr;i++) if(in[i]==post[por]) {pos=i; break;}
    print_pre(inl,pos-1,pol,pol+pos-inl-1);
    print_pre(pos+1,inr,pol+pos-inl,por-1);
    return;
}

int main(){
    RES(in,0); RES(post,0);
    scanf("%s",in); scanf("%s",post);
    len=strlen(in)-1;
    print_pre(0,len,0,len);
    return 0;
}

求中序(重点)

已知:
前序ABDEFGCH
后序FGEDBHCA

数组存储转链表存储

struct tree{
    struct tree *left;
    int data;
    struct tree *right;
};
typedef struct tree treenode;
typedef struct tree *b_tree;

//和之前一样定义二叉树节点的结构体 
int N;//N为数组结构的长度 

b_tree create(int *node, int position){
    b_tree newnode;
    if(node[position]==0||position>N)//根节点和不存在的节点都是NULL 
        return NULL;
    else{
        newnode=(b_tree)malloc(sizeof(treenode));
        newnode->data=node[postion];
        newnode->left=create(node,2*position);
        newnode->right=create(node,2*position+1);
        return newnode;//否则递归建树 
    }
} 

二叉查找树

最开始用链表建树的时候你发现了它已经是一颗二叉查找树了吗?二叉查找树满足如下三点性质:

  • 左子树中的节点小于根节点
  • 右子树中的节点大于根节点
  • 左右子树仍是二叉平衡树

要实现以下三种操作

  • 查找
  • 插入
  • 删除

Treap——随机平衡二叉查找树

因为二叉查找树可能会因为数据的原因退化成一条链,查找效率变低,所以Treap的原理就是给每一个节点分配一个随机的关键词,对于关键词Treap要满足堆的性质,对于权值要满足二叉搜索树的性质——构造了一个随机平衡的二叉查找树。

节点定义

六个参数:左儿子、右儿子、权值key、用来平衡的随机数fix、该节点重复的数据cnt、以该节点为根节点的子树大小size

struct Node{
    Node *left, *right;//左右儿子递归定义
    int key,fix,cnt,size;
    Node (int i) : key(i),fix(rand()),size(1),cnt(1) {}//初始化函数
}*root,*null//根节点和空节点

堆的性质

  • 大根堆:根节点大于儿子节点
  • 小根堆:根节点小于儿子节点
  • 左右子树仍然是一个堆

插入

先做二叉查找树的插入,然后再调整使其满足堆的性质

  • 左旋:右节点到根节点,根节点到左节点(当前结点是根的右儿子)即右节点提升
  • 左旋操作:要旋转的子树根节点编号为a
void left_rotate(node* &a){
    node *b=a->right;
    a->right=b->left;
    b->left=a;
    a=b;
}
  • 右旋:左节点到根节点,根节点到右节点(当前结点是根的左儿子)即左节点提升
  • 右旋操作:要旋转的子树根节点编号为a
void right_rotate(node* &a){
    node* b=a->left;
    a->left=b->right;
    b->right=a;
    a=b;
}

插入操作O(logn),旋转操作最坏O(h)

删除

把待删除节点旋转到只有一个子节点的地方删除即可。正如上一小节提到的一样,左旋可以使右节点提升而右旋可以使左节点提升,引出如下两种操作。
- 当当前结点的左节点小于右节点时,右旋
- 当当前结点的右节点小于左节点时,左旋
- 直到当前结点子节点个数小于等于一时,直接删除即可

查找

就当二叉搜索树查找就好了

  • 如果当前结点大于查找值,在左子树中查找
  • 如果当前结点小于查找值,在右子树中查找

应用

求前驱和后继
  • 前驱:该元素在平衡树中不大于该元素的最大元素
  • 后继:该元素在平衡树中不小于该元素的最小元素

方法(以前驱为例):

  1. 以根节点为当前结点,最优值设为空
  2. 如果当前结点不大于该元素,更新最优值(如果该节点更接近该元素就更新,否则不做变动),访问当前结点的右子树
  3. 如果当前结点大于该元素,访问当前结点的左子树
  4. 直到当前结点是空节点为止

下面都需要维护子树的大小
对于旋转、插入、删除三种操作

  • 旋转:
  • 插入:
  • 删除:
查找第k小的元素
询问某个元素从小到大的排名

有时间了搞这道题

哈夫曼树(最优二叉树)

定义

带权路径长度:从根结点到叶子结点的长度与叶子结点数据的乘积
对一一些给定权值的节点,具有最小带权路径长度的二叉树叫做哈夫曼树

构造

权值越大的叶子结点越靠近根节点

  1. 生成树的集合(裸的只有一个节点的树)
  2. 在集合中选取根节点最小和次小的两棵二叉树分别作为左右子树,根节点权值为左右子树根节点权值之和
  3. 在二叉树集合中删除左右两子树,将新生成的二叉树加入集合
  4. 重复2、3直到剩下一颗二叉树即为所求的哈夫曼树
posted @ 2016-11-03 08:58  Leo.Tan  阅读(1107)  评论(0编辑  收藏  举报