数据结构与算法(四)
中缀表达式转换为后缀表达式
- 总结规则:从左到右遍历中缀表达式的每个数字和符号,若是数字则直接输出,若是符号,则判断其与栈顶符号的优先级,是右括号或者优先级低于栈顶符号,则栈顶元素依次出栈并输出,直到遇到左括号或者栈空才将原先的符号入栈。
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define STACK_INIT_SIZE 20
#define STACKINCREMENT 10
typedef char ElemType;
typedef struct
{
ElemType *base;
ElemType *top;
int stackSize;
}sqStack;
void InitStack(sqStack *s)
{
s->base = (ElemType *)malloc(STACK_INIT_SIZE * sizeof(ElemType));
if( !s->base )
{
exit(0);
}
s->top = s->base;
s->stackSize = STACK_INIT_SIZE;
}
void Push(sqStack *s, ElemType e)
{
if( s->top - s->base >= s->stackSize )
{
s->base = (ElemType *)realloc(s->base, (s->stackSize + STACKINCREMENT) * sizeof(ElemType) );
if( !s->base )
{
exit(0);
}
}
*(s->top) = e;
s->top++;
}
void Pop(sqStack *s, ElemType *e)
{
if( s->top == s->base )
{
return;
}
*e = *--(s->top);
}
int StackLen(sqStack s)
{
return (s.top - s.base);
}
int main()
{
sqStack s;
char c, e;
InitStack( &s );
printf("请输入中缀表达式,以#作为结束标志: ");
scanf("%c", &c);
while(c != '#' )
{
while( c >= '0' && c <= '9' )
{
printf("%c", c);
scanf("%c", &c);
if( c < '0' || c > '9' )
{
printf(" ");
}
}
if ( ')' == c )
{
Pop(&s, &e);
while('(' != e )
{
printf("%c ", e);
Pop(&s, &e);
}
}
else if( '+' == c || '-' == c )
{
if( !StackLen(s) )
{
Push(&s, c);
}
else
{
do
{
Pop(&s, &e);
if( '(' == e )
{
Push( &s, e );
}
else
{
printf("%c ", e );
}
}while( StackLen(s) && '(' != e );
Push(&s, c);
}
}
else if( '*' == c || '/' == c || '(' == c )
{
Push( &s, c );
}
else if( '#' == c )
{
break;
}
else
{
printf("\n出错:输入格式错误!\n");
return -1;
}
scanf("%c", &c);
}
while( StackLen(s) )
{
Pop(&s, &e);
printf("%c ", e);
}
return 0;
}
队列
-
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
-
与栈相反,队列是一种先进先出(First In First Out,FIFO)的线性表。
-
与栈相同的是,队列也是一种重要的线性结构,实现一个队列同样需要顺序表或链表作为基础。
-
队列既可以用链表实现,也可以用顺序表来实现。但队列一般使用链表来实现,简称链队列。
typedef struct QNode{ ElemType data; struct QNode *next; }QNode, *QueuePrt; typedef struct { QueuePrt front, rear; //队头、尾指针 } LinkQueue;
-
队列的链式存储结构:将队头指针指向链队列的头结点,而队尾指针指向终端结点。(注:头结点不是必要的)
创建一个队列
-
创建一个队列要完成两个任务:一是在内存中创建一个头结点,二是将队列的头指针和尾指针都指向这个生成的头结点,因为此时是空队列。
initQueue( LinkQueue *q ) { q->front = q->rear = ( QueuePtr )malloc( sizeof( QNode ) ); if( !q->front ) exit(0); q->front->next = NULL; }
入队列操作
InsertQueue(LinkQueue *q, ElemType e)
{
QueuePtr p;
p = (QueuePtr)malloc(sizeof(QNode));
if( p == NULL )
exit(0);
p->data = e;
p->next = NULL;
q->rear->next = p;
q->rear = p;
}
出队列操作
-
出队列操作是将队列中的第一个元素溢出,队头指针不发生改变,改变头结点的next指针即可。
-
操作过程如下:
-
如果原队列只有一个元素,我们就应该处理一下队尾指针。
DeleteQueue( LinkQueue *q, ElemType *e )
{
QueuePtr p;
if( q->front == q->rear )
return;
p = q->front->next;
*e = p->data;
q->front->next = p->next;
if( q->rear == p )
q->rear = q->front;
free(p);
}
销毁一个队列
- 由于链队列建立在内存的动态去,因此当一个队列不再有用时应当把它及时销毁掉,以免过多地占用内存空间。
DestroyQueue( LinkQueue *q )
{
while( q->front ){
q->rear = q->front->next;
free( q->front );
q->front = q->rear;
}
}
循环队列
- 循环队列的容量是固定的,并且它的队头和队尾指针都可以随着元素入出队列而发生改变,逻辑上就像是一个环形存储空间。
- 但实际中,不可能有真正的环形存储区,只是用顺序表模拟出来的逻辑上的循环。
- 循环队列的实现只需要灵活改变front和rear指针即可
- 也就是让front或rear指针不断加1,即使超出了地址范围,也会自动从头开始,可以采取取模运算处理。
定义一个循环队列
#define MAXSIZE 100
typedef struct{
ElemType *base; //用于存放内存分配基地址,也可以用数组来存放
int front;
int rear;
}
初始化一个循环队列
initQueue(cycleQueue *q){
q->base = (ElemType *)malloc(MAXSIEZ * sizeof(ElemType));
if( !q->base )
exit(0);
q->front = q->next = 0;
}
入队列操作
InsertQueue(cycleQueue *q, ElemType e)
{
if( (q->rear+1)%MAXSIZE == q->front )
return; //队列已满
q->base[q->rear] = e;
q->rear = (q->rear+1) % MAXSIZE;
}
递归和分治思想
斐波那契数列(使用迭代)
#include <stdio.h>
int main()
{
int i;
int a[40];
a[0] = 0;
a[1] = 1;
printf("%d %d ", a[0], a[1]);
for( i=2; i < 40; i++ )
{
a[i] = a[i-1] + a[i-2];
printf("%d ", a[i]);
}
return 0;
}
斐波那契数列(使用递归)
递归事实上就是函数自己调用自己
int Fib(int i)
{
if(i < 2 )
return i == 0? 0:1;
return Fib(i-1) + Fib(i-2);
}
汉诺塔问题(使用递归)
void move(int n, char x, char y, char z)
{
if( 1 == n )
{
printf("%c---->%c\n", x, z);
}
else
{
move(n-1, x, z, y); //将n-1个盘子从x借助z移动到y上
printf("%c---->%c\n", x, z); //将第n个盘子从x移到z上
move(n-1, y, x, z); //将n-1个盘子从y借助x移到z上
}
}
八皇后问题
8*8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处在同一行、同一列或者是同一斜线上,问有多少种摆法。
#include <stdio.h>
int count = 0;
int notDanger( int row, int j, int (*chess)[8] )
{
int i, k, flag1=0, flag2=0, flag3=0, flag4=0, flag5=0;
// 判断列方向
for( i=0; i < 8; i++ )
{
if( *(*(chess+i)+j) != 0 )
{
flag1 = 1;
break;
}
}
// 判断左上方
for( i=row, k=j; i>=0 && k>=0; i--, k-- )
{
if( *(*(chess+i)+k) != 0 )
{
flag2 = 1;
break;
}
}
// 判断右下方
for( i=row, k=j; i<8 && k<8; i++, k++ )
{
if( *(*(chess+i)+k) != 0 )
{
flag3 = 1;
break;
}
}
// 判断右上方
for( i=row, k=j; i>=0 && k<8; i--, k++ )
{
if( *(*(chess+i)+k) != 0 )
{
flag4 = 1;
break;
}
}
// 判断左下方
for( i=row, k=j; i<8 && k>=0; i++, k-- )
{
if( *(*(chess+i)+k) != 0 )
{
flag5 = 1;
break;
}
}
if( flag1 || flag2 || flag3 || flag4 || flag5 )
{
return 0;
}
else
{
return 1;
}
}
// 参数row: 表示起始行
// 参数n: 表示列数
// 参数(*chess)[8]: 表示指向棋盘每一行的指针
void EightQueen( int row, int n, int (*chess)[8] )
{
int chess2[8][8], i, j;
for( i=0; i < 8; i++ )
{
for( j=0; j < 8; j++ )
{
chess2[i][j] = chess[i][j];
}
}
if( 8 == row )
{
printf("第 %d 种\n", count+1);
for( i=0; i < 8; i++ )
{
for( j=0; j < 8; j++ )
{
printf("%d ", *(*(chess2+i)+j));
}
printf("\n");
}
printf("\n");
count++;
}
else
{
for( j=0; j < n; j++ )
{
if( notDanger( row, j, chess ) ) // 判断是否危险
{
for( i=0; i < 8; i++ )
{
*(*(chess2+row)+i) = 0;
}
*(*(chess2+row)+j) = 1;
EightQueen( row+1, n, chess2 );
}
}
}
}
int main()
{
int chess[8][8], i, j;
for( i=0; i < 8; i++ )
{
for( j=0; j < 8; j++ )
{
chess[i][j] = 0;
}
}
EightQueen( 0, 8, chess );
printf("总共有 %d 种解决方法!\n\n", count);
return 0;
}
字符串
- 定义:串(String)是由零个或者多个字符组成的有限序列,又名字符串。
- 串可以是空串,即没有字符。
字符串的存储结构
- 字符串的存储结构与线性表相同,也分顺序存储结构和链式存储结构。
- 字符串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的
- 按照预定义的大小,为每个定义的字符串变量分配一个固定长度的存储区,一般用定长数组来定义。
BF算法(Brute Force)
- 朴素的模式匹配算法
- 核心思想:有两个字符串S和T,长度为N和M。首先S[1]和T[1]比较,若相等,则再比较S[2]和T[2],一直到T[M]为止;若S[1]和T[1]不等,则T向右移动一个字符的位置,再依次进行比较。S为主串,T为子串。
- 该算法最坏的情况下要进行M(N-M+1)次比较,时间复杂度为O(M*N).
- 实现例子:
/*自行实现的代码*/
int BF(char S[], char T[])
{
int i, j;
i = j = 0;
while(S[i] != '\0' &&T[j] != '\0')
{
if(S[i] == T[i])
{
i++;
j++;
}
else
{
j = 0;
i = i-j+1;
}
}
if(T[j] == '\0')
return i-j+1;
else
return 0;
}
/*依靠String实现的BF算法*/
int BFstring(string MotherStr, string SonStr){
int i =0, j =0;
for(;(i != MotherStr.size()) && (j != SonStr.size());){
if(MotherStr[i] == SonStr[j]){
i++, j++;
}
else{
i = i - j + 1;
j = 0;
}
if(j == SonStr.size()){
return i - j + 1;
}
}
return 0;
}
KMP算法
(详情看笔记KMP算法)
树
-
树(Tree)是n(n>=0)个结点的有限集。当n=0时成为空树,在任意一棵非空树中:
- 有且仅有一个特定的称为根(Root)的结点;
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、...、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
- n>0时,根结点是唯一的,不可能存在多个根结点。
- m>0时,子树的个数是没有限制的,但它们互相是一定不会相交的。
- 每个圈圈称为树的一个结点。结点拥有的子树数称为结点的度(Degree),树的度取树内各结点的度的最大值。
- 度为0的结点称为叶结点(Leaf)或终端结点
- 度不为0的结点称为分支结点或非终端结点,除根结点外,分支结点也称为内部结点
-
结点之间的关系
- 结点的子树的根称为结点的孩子(Child),相应的,该结点称为孩子的双亲(Parent),同一双亲的孩子之间互称为兄弟(Sibling)。
- 结点的祖先是从根到该结点所经分支上的所有结点。
-
结点的层次
-
结点的层次从根开始定一起,根为第一层,根的孩子为第二层。
-
其双亲在同一层的结点互为堂兄弟
-
树中结点的最大层次称为树的深度(Depth)或高度
-
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
-
森林(Forest)是m(m>=0)棵互不相交的树的集合。对于树中每个结点而言,其子树的集合即为森林。
-
-
树的存储结构
-
三种不同的表示法:双亲表示法,孩子表示法,孩子兄弟表示法
-
双亲表示法:
-
以双亲作为索引的关键词的一种存储方式。
-
假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示其双亲结点在数组中位置的元素。
-
定义:
-
//树的双亲表示法结点结构定义 #define MAX_TREE_SIZE 100 typedef int ElemType; typedef struct PTNode { ElemType data; //结点数据 int parent; //双亲位置 }PTNode; typedef struct { PTNode nodes[MAX_TREE_SIZE]; int r; //根的位置 int n; //结点数目 }PTree;
-
-
双亲孩子表示法
-
#define MAX_TREE_SIZE 100 typedef char ElemType; //孩子结点 typedef struct CTNode { int child; //孩子结点的下标 struct CTNode *next; //指向下一个孩子结点的指针 } *ChildPtr; //表头结构 typedef struct { ElemType data; //存放在树中的结点的数据 int parent; //存放双亲的下标 ChildPtr firstchild; //指向第一个孩子的指针 } CTBox; //树结构 typedef struct { CTBox nodes[MAX_TREE_SIZE]; //结点数组 int r, n; }
-
二叉树
-
二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
-
每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
-
左子树和右子树是有顺序的,次序不能颠倒。
-
二叉树的五种基本形态:
满二叉树
-
特点:
- 叶子只能出现在最下一层
- 非叶子结点的度一定是2
- 在同样深度的二叉树中,满二叉树的结点个数一定最多,同时叶子也是最多。
完全二叉树
-
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点位置完全相同,则这棵二叉树称为完全二叉树。
-
特点:
- 叶子结点只能出现在最下两层
- 最下层的叶子一定集中在左部连续位置
- 倒数第二层,若有叶子结点,一定都在右部连续位置
- 如果结点度为1,则该结点只有左孩子
- 同样结点树的二叉树,完全二叉树的深度最小。
-
注意:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
-
反例:
二叉树的遍历
-
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问依次且仅被访问一次。
-
二叉树的遍历方式:前序遍历、中序遍历、后序遍历、层序遍历
- 前序遍历:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。前序遍历方式如图所示
- 中序遍历:若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
- 后序遍历:若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点。
- 层序遍历:若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
-
二叉树的建立和遍历算法
-
#include <stdio.h> #include <stdlib.h> typedef char ElemType; typedef struct BiTNode { char data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; //创建一棵二叉树,约定用户遵照前序遍历的方式输入数据 CreateBiTree(BiTree *T) { char c; scanf("%c", &c); if( ' ' == c ) { *T = NULL; } else { *T = (BiTNode *)malloc(sizeof(BiTNode)); (*T)->data = c; CreateBiTree(&(*T)->lchild); CreateBiTree(&(*T)->rchild); } } //访问二叉树结点的具体操作 visit(char c, int level) { printf("%c 位于第 %d层\n", c, level); } //前序遍历二叉树 PreOrderTraverse(BiTree T, int level) { if( T ) { visit(T->data, level); PreOrderTraverse(T->lchild, level+1); PreOrderTraverse(T->rchild, level+1); } } int main() { int level = 1; BiTree T = NULL; CreateBiTree(&T); PreOrderTraverse(T, level); }
线索二叉树
#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
//线索存储标志位
//Link(0):表示指向左右孩子的指针
//Thread(1):表示指向前驱后继的线索
typedef enum{Link, Thread} PointerTag;
typedef struct BiThrNode
{
char data;
struct BiThrNode *lchild, *rchild;
PointerTag ltag;
PointerTag rtag;
} BiThrNode, *BiThrTree;
//全局变量,始终指向刚刚访问过的结点
BiThrTree pre;
//创建一棵二叉树,约定用户遵照前序遍历的方式输入数据
CreateBiThrTree( BiThrTree *T )
{
char c;
scanf("%c", &c);
if( ' ' == c )
{
*T = NULL;
}
else
{
*T = (BiThrNode *)malloc(sizeof(BiThrNode));
(*T)->data = c;
(*T)->ltag = Link;
(*T)->rtag = Link;
CreateBiThrTree(&(*T)->lchild);
CreateBiThrTree(&(*T)->rchild);
}
}
//中序遍历线索化
InThreading(BiThrTree T)
{
if( T )
{
InThreading(T->lchild ); //递归左孩子线索化
//结点处理
if( !T->lchild ) //如果该结点没有左孩子,设置ltag为Thread,并把lchild指向刚刚访问的结点。
{
T->ltag = Thread;
T->lchild = pre;
}
if( !pre->rchild )
{
pre->rtag = Thread;
pre->rchild = T;
}
pre = T;
InThreading( T->rchild ); //递归右孩子线索化
}
}
InOrderThreding( BiThrTree *p, BiThrTree T )
{
*p = (BiThrTree)malloc(sizeof(BiThrNode));
(*p)->ltag = Link;
(*p)->rtag = Thread;
(*p)->rchild = *p;
if( !T )
{
(*p)->lchild = *p;
}
else
{
(*p)->lchild = T;
pre = *p;
InThreading(T);
pre->rchild = *p;
pre->rtag = Thread;
(*p)->rchild = pre;
}
}
void visit( char c )
{
printf("%c", c);
}
//中序遍历二叉树,非递归
void InOrderTraverse( BiThrTree T )
{
BiThrTree p;
p = T->lchild;
while(p != T )
{
while( p->ltag == Link )
{
p = p->lchild;
}
visit(p->data);
while(p->rtag == Thread && p->rchild != T )
{
p = p->rchild;
visit(p->data);
}
p = p->rchild;
}
}
int main()
{
BiThrTree P, T = NULL;
CreateBiThrTree( &T );
InOrderThreading( &P, T );
printf("中序遍历输出结果为:");
InOrderTraverse( P );
printf("\n");
return 0;
}
树和森林
普通树转换为二叉树
- 加线,在所有兄弟结点之间加一条连线。
- 去线,对树中每个结点,只保留它与第一孩子结点的连线,删除它与其他孩子结点之间的连线
- 层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
森林转换为二叉树
- 把每棵树都转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
树与森林的遍历
-
树的遍历分为两种方式:一种是先根遍历,另一种是后根遍历
-
先根遍历:先访问树的根结点,然后再依次先根遍历根的每棵子树。
-
后根遍历:先依次遍历每棵子树,然后再访问根结点。
-
森林的遍历也分前序遍历和后序遍历,其实就是按照树的先根遍历和后根遍历依次访问森林的每一棵树。
-
树、森林的前根(序)遍历和二叉树的前序遍历结果相同,树、森林的后根(序)遍历和二叉树的中序遍历结果相同。