基本数据结构:
一.线性表
1.顺序结构
线性表可以用普通的一维数组存储。
你可以让线性表可以完成以下操作(代码实现很简单,这里不再赘述):
- 返回元素个数。
- 判断线性表是否为空。
- 得到位置为p的元素。
- 查找某个元素。
- 插入、删除某个元素:务必谨慎使用,因为它们涉及大量元素的移动。
2.链式结构
(1) 单链表:
1.定义:下面有一个空链表,表头叫head,并且表内没有任何元素。
struct node { int value; node *next; } arr[MAX]; int top=-1; node *head = NULL;
2.内存分配:在竞赛中不要用new,也不要用malloc、calloc——像下面一样做吧。
#define NEW(p) p=&arr[++top];p->value=0;p->next=NULL node *p; NEW(head); // 初始化表头 NEW(p); // 新建结点
3.插入:把q插入到p的后面。时间复杂度O(1)。
if (p!=NULL && q!=NULL) // 先判定是否为空指针。如果不是,继续。 { q->next=p->next; p->next=q; }
4.删除:把p的下一元素删除。时间复杂度O(1)。
if (p!=NULL && p->next!=NULL) // 先判定是否为空指针。如果不是,继续。 { node *q=p->next; p->next=q->next; // delete(q); // 如果使用动态内存分配,最好将它的空间释放。 }
5.查找或遍历:时间复杂度O(n)。
node *p=first; while (p!=NULL) { // 处理value // coutvalue p=p->next; }
(2) 静态链表
指针的作用就是存储地址。如果我们找到了替代品,就可以放弃指针了。
需要把上面的定义改一下:
struct node { int value; int next; // 表示下一元素在arr中的下标 } arr[MAX];
(3) 循环链表
和单链表有一个重大区别:单链表最后一个元素的next指向NULL,而循环链表最后一个元素的next指向first。
遍历时要留心,不要让程序陷入死循环。
一个小技巧:如果维护一个表尾指针last,那么就可以在O(1)的时间内查找最后一个元素。同时可以防止遍历时陷入死循环。
(4) 双链表
1.定义:下面有一个空链表,表头叫first。
struct node { int value; node *next, *prev; } arr[MAX]; int top=-1; node *first = NULL; // 根据实际需要可以维护一个表尾指针last。
2.内存分配:最好不要使用new运算符或malloc、calloc函数。
#define NEW(p) p=arr+(++top);p->value=0;p->next=NULL;p->prev=NULL node *p; NEW(head); // 初始化表头 NEW(p); // 新建结点
3.插入:把q插入到p的后面。时间复杂度O(1)。
if (p==NULL||q==NULL) // 先判定是否为空指针。如果不是,继续。 { q->prev=p; q->next=p->next; q->next->prev=q; p->next=q; }
4.删除:把p的下一元素删除。时间复杂度O(1)。
if (p==NULL||p->next==NULL) // 先判定是否为空指针。如果不是,继续。 { node *q=p->next; p->next=q->next; q->next->prev=p; // delete(q); // 如果使用动态内存分配,最好将它的空间释放。 }
5.查找或遍历:从两个方向开始都是可以的。
(5) 将元素插入到有序链表中*
void insert(const node *head, node *p) { node *x, *y; y=head; do { x=y; y=x->next; } while ((y!=NULL) && (y->value value); x->next=p; p->next=y; }
二.栈
(1) 栈的实现!
操作规则:先进后出,先出后进。
int stack[N], top=0; // top表示栈顶位置。
入栈:inline void push(int a) { stack[top++]=a; }
出栈:inline int pop() { return stack[–top];
栈空的条件:inline bool empty() { return top
如果两个栈有相反的需求,可以用这种方法节省空间:用一个数组表示两个栈。分别用top1、top2表示栈顶的位置,令top1从0开始,top2从N-1开始。
(2) DFS和栈
递归其实也用到了栈。每调用一次函数,都相当于入栈(当然这步操作“隐藏在幕后”)。函数调用完成,相当于出栈。
一般情况下,调用栈的空间大小为16MB。也就是说,如果递归次数太多,很容易因为栈溢出导致程序崩溃,即“爆栈”。
为了防止“爆栈”,可以将递归用栈显式实现。如果可行,也可以改成迭代、递推等方法。
使用栈模拟递归时,注意入栈的顺序——逆序入栈,后递归的要先入栈,先递归的要后入栈。
下面是非递归版本的DFS模板:
stack int> s; // 存储状态 void DFS(int v, …) { s.push(v); // 初始状态入栈 while (!s.empty()) { int x = s.top(); s.pop(); // 获取状态 // 处理结点 if (x达到某种条件) { // 输出、解的数量加1、更新目前搜索到的最优值等 … return; } // 寻找下一状态。当然,不是所有的搜索都要这样寻找状态。 // 注意,这里寻找状态的顺序要与递归版本的顺序相反,即逆序入栈。 for (i=n-1;i>=0;i--) { s.push(… /*i对应的状态*/); } } // 无解 cout"No Solution."; }
三.队列
(1) 顺序队列
操作规则:先进先出,后进后出。
定义:int queue[N], front=0, rear=0;
front指向队列首个元素,rear指向队列尾部元素的右侧。
入队:inline void push(int a) { queue[rear++]=a; }
出队:inline int pop() { return queue[front++]; }
队空的条件:inline bool empty() { return front==rear; }
(2) 循环队列
循环队列——把链状的队列变成了一个环状队列。与上面的链状队列相比,可以节省很大空间。
定义:int queue[N], front=0, rear=0;
front指向队列首个元素,rear指向队列尾部元素的右侧。
入队:inline void push(int a) { queue[rear]=a; rear=(rear+1)%N; }
出队:inline int pop() { int t=queue[front]; front=(front+1)%N; return t; }
队满或队空的条件:inline bool empty() { return front==rear; }
队满和队空都符合上述条件。怎么把它们区分开呢?
第一种方法:令队列的大小是N+1,然后只使用N个元素。这样队满和队空的条件就不一样了。
第二种方法:在入队和出队同时记录队列元素个数。这样,直接检查元素个数就能知道队列是空还是满。
(3) BFS和队列
BFS要借助队列来完成,并且,将队列改成堆栈,BFS就变成了DFS。BFS的具体实现见42页“3.7 代码模板”。
四.二叉树
(1) 二叉树的链表存储法
struct node { int value; node *leftchild, *rightchild; //int id; // 结点编号。 //node *parent; // 指向父亲结点。 } arr[N]; int top=-1; node * head = NULL; #define NEW(p) p=&arr[++top]; p->leftchild=NULL; p->rightchild=NULL; p->value=0
(2) 完全二叉树的一维数组存储法
如果一个二叉树的结点严格按照从上到下、从左到右的顺序填充,就可以用一个一维数组保存。
下面假设这个树有n个结点,待操作的结点是r(0≤r<n)。
操作 |
宏定义 |
r的取值范围 |
r的父亲 |
#define parent(r) (((r)-1)/2) |
r≠0 |
r的左儿子 |
#define leftchild(r) ((r)*2+1) |
2r+1<n |
r的右儿子 |
#define rightchild(r) ((r)*2+2) |
2r+2<n |
r的左兄弟 |
#define leftsibling(r) ((r)-1) |
r为偶数且0<r≤n-1 |
r的右兄弟 |
#define rightsibling(r) ((r)+1) |
r为奇数且r+1<n |
判断r是否为叶子 |
#define isleaf(r) ((r)>=n/2) |
r<n |
(3) 二叉树的遍历
1. 前序遍历
void preorder(node *p) { if (p==NULL) return; // 处理结点p coutvalue' '; preorder(p->leftchild); preorder(p->rightchild); }
2. 中序遍历
void inorder(node *p) { if (p==NULL) return; inorder(p->leftchild); // 处理结点p coutvalue' '; inorder(p->rightchild); }
3. 后序遍历
void postorder(node *p) { if (p==NULL) return; postorder(p->leftchild); postorder(p->rightchild); // 处理结点p coutvalue' '; }
假如二叉树是通过动态内存分配建立起来的,在释放内存空间时应该使用后序遍历。
4. 宽度优先遍历(BFS)
首先访问根结点,然后逐个访问第一层的结点,接下来逐个访问第二层的结点……
node *q[N]; void BFS(node *p) { if (p==NULL) return; int front=1,rear=2; q[1]=p; while (frontrear) { node *t = q[front++]; // 处理结点t coutvalue' '; if (t->leftchild!=NULL) q[rear++]=t->leftchild; if (t->rightchild!=NULL) q[rear++]=t->rightchild; } }
对于完全二叉树,可以直接遍历:
for (int i=0; i' ';
(4) 二叉树重建
【问题描述】二叉树的遍历方式有三种:前序遍历、中序遍历和后序遍历。现在给出其中两种遍历的结果,请输出第三种遍历的结果。
【分析】
前序遍历的第一个元素是根,后序遍历的最后一个元素也是根。所以处理时需要到中序遍历中找根,然后递归求出树。
注意!输出之前须保证字符串的最后一个字符是‘