【游戏开发笔记】编程篇_数据结构
@
1.线性表
操作:
1、创建和初始化
2、重置为空表
3、按位序查找
4、按名称查找
5、获取线性表长度
6、插入和删除
1.1 顺序表
属性:
起始位置(数组首地址)
最大存储容量(数组长度)
当前长度(数组初始化)
数组长度和线性表长度区别:
数组的最大存储容量和当前长度不一定相等,线性表最大存储容量即当前长度
优点:
增加元素与逻辑无关
存取速度快
缺点:
插入和删除繁琐
有时难以确定存储容量,造成存储空间碎片
-
插入
顺序表插入算法思路:- 如果插入的位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,抛出异常或动态增加数组长度
- 从最后一个元素开始向前遍历到第 i 个位置,分别将它们都向后移动一个位置,
- 将要插入的元素填入位置 i 处
- 表长加1
-
删除
顺序表删除算法思路:- 如果删除位置不合理,抛出异常
- 取出删除元素
- 从删除元素位置开始到最后一个元素位置,分别将他们都向前移动一个位置。
- 表长减1
1.2 链表
实现:顺序表每个成员增加了一个尾指针
除了存储自身信息(数据域),还存储后继位置的域(指针域)。这两部分组成数据元素的存储映像(结点)
头指针和头结点:
头结点——在链表之前再加一个结点,方便操作链表
头指针——头结点的指针域,指向第一个结点
静态链表:
用数组代替指针,描述单链表
(是单链表的半成品,同时抛弃了顺序存储结构随机存储的特性)
1.2.1单链表
-
读取
单链表获取链表第i个数据的算法思路:- 声明一个结点P指向第i个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若链表末尾p为空,说明第i个元素不存在
- 否则查找成功,返回结点p的数据
-
插入
单链表第i个数据插入结点算法思路:- 声明一结点p指向链表第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,就说明第i个元素不存在
- 否则查找成功,在系统中生成一个空结点s
- 将数据元素e赋值给
s -> data
- 单链表的插入标准语句:
s -> next = p -> next; p -> next = s; //先让空结点的指针域指向p的下一个结点,再p结点的指针域指向空结点
- 返回成功
-
删除
单链表第i个数据删除结点的思路:- 声明一结点p指向链表第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾p为空,就说明第i个元素不存在
- 否则查找成功,将欲删除的结点
p->next
赋给q - 将数据元素e赋值给
s -> data
- 单链表的删除标准语句:
p -> next = q -> next; // 将欲删除结点的指针域赋给上一个结点的指针域
- 将q结点中的数据赋值给e,作为返回
- 释放q结点
- 返回成功
-
创建
单链表整表创建的算法思路:- 声明一结点p和计数器变量i
- 初始化一空链表L
- 让L的头结点的指针指向NULL,即创建一个带头结点的单链表
- 循环
- 生成一新结点赋值给p
- 随机生成一数字赋值给p的数据域:
p -> data;
- 将p插入到头结点与新结点之间
-
全删
单链表整表删除算法思路:- 声明一结点p和q
- 将第一个结点赋值给p
- 循环:
- 将下一个结点赋值个q
- 释放p
- 将q赋值给p
1.2.2循环链表
实现:单链表尾指针指向头结点
- 合并
链表一:头结点的尾指针:rear -> next
链表二:头结点:rearB -> next
, 结点一:rearB -> next -> next
实现:
p = rearA -> next; /*保存A表的头结点*/
rearA -> next = rearB -> next -> next; /*A表尾指针指向B表结点一*/
rearB -> next = p; /*将B表的尾指针指向A表*/
free(p); /*释放p*/
1.2.3双向链表
实现:循环链表每个结点增加了一个尾指针
typedef struct DulNode
{
ElemType data;
struct DulNode *prior; /*直接前驱指针*/
struct DulNode *next; /*直接后继指针*/
}DulNode, *DuLinkList;
内存换取速度
- 插入
s -> prior = p; /*将需插入结点的前驱指向前结点后继*/
s -> next = p -> next; /*将需插入结点的后继指向后结点的前驱*/
p -> next -> prior = s; /*将后结点的前驱指向需插入结点后继*/
p -> next = s; /*将前结点的后继指向需插入结点前驱*/
- 删除
p -> prior -> next = p -> next; /*将将需删除结点的前结点后驱指向 需删除结点的后结点的前驱*/
p -> next -> prior = p -> prior; /*将需删除结点的后结点的前驱指向 需删除结点的前结点的后继*/
free(p);
2.栈
是一个线性表,是限定仅在表尾插入和删除的线性表,是后进先出的线性表。
表尾——栈顶(top)
表头——栈底(bottom)
最先进栈的元素不一定最后出栈
如:1,2,3,
1进,1出,2进,2出,3进,3出。出栈次序为123。
但没有312的出栈情况,因为3最先出栈,意味着12已经进去了,此时,2就在1的上面。
push压栈,pop弹栈
变化范围可控,则使用顺序栈,变化时大时小,使用链栈,两组变化呈线性反相关关系,可以考虑使用共享栈
2.1 顺序栈
(数组)下标为0的一端作为栈底
故空栈判定条件:top = -1
- 定义
栈的结构定义:
typedef int SElemType; /*SElemType根据情况而定,这里选择int*/
typedef struct
{
SElemType data[MAXSIZE];
int top; /*用于栈顶指针*/
}SqSeack;
- push
/*插入元素e为新栈顶元素*/
Status Push( SqStack *S, SElemType e);
{
if( S -> top == MAXSIZE -1)
{
return ERROR;
}
S -> top++;
S -> data[ S-> top] = e;
return OK;
}
- pop
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR*/
Status Pop( SqStack *S, SElemType *e)
{
if( S -> top == -1)
{
return ERROR;
}
*e = S->data[ S->top]; /*将要删除的栈顶元素赋值给e*/
S -> top--; /*栈顶指针减一*/
return OK;
}
2.2 链栈
- 定义
typedef struct StackNode
{
SElemType data;
struct StackNode *next;
}StackNode, *LinkStackPtr;
typedef struct LinkStack
{
LinkStackPtr top;
int count;
}LinkStack;
- push
/*插入元素e作为新栈顶元素*/
Status Push( LinkStack *S, SElemType e)
{
LinkStackPtr s = ( LinkStackPtr) malloc( sizeof( StackNode));
s -> data = e;
s -> next = S -> top; /* 把当前栈顶元素赋值给新节点的直接后继*/
S -> top = s; /* 将新节点s赋值给栈顶指针*/
S -> count++;
return OK;
}
- pop
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR*/
Status Pop( LinkStack *S, SElemType *e)
{
LinkStackPtr pr;
if( StackEmpty( *S))
return ERROR;
*e = S -> top -> data;
p = S -> top; /* 将栈顶结点赋值给p*/
S -> top = S -> top -> next; /* 使得栈顶指针下移一位,指向后一结点*/
free( p); /* 释放结点p*/
S -> counter--;
return OK;
}
2.3 共享栈
关键思路:两栈是在数组的两端,向中间靠拢,top1和top2是栈1和栈2 的栈顶指针,只要他俩不见面,两栈就可以一直使用。
使用需求;空间需求成反比的相同数据类型
- 定义
/*两栈共享空间结构*/
typedef struct
{
SElemType data[ MAXSIZE];
int top1; /* 栈1栈顶指针*/
int top2; /* 栈2栈顶指针*/
}SqDoubleStack;
- push
/*插入元素e为新栈的栈顶元素*/
Status Push( SqDoubleStack *S, SElemType *e, int stackNumber)
{
if( S->top+1 == S->top2)
return ERROR;
if( stackNumber == 1) /*栈1有元素进栈*/
S -> data[ ++S- > top] = e; /*先top+1后给数组元素赋值*/
else if( stackNumber == 2) /*栈2有元素进栈*/
S -> data[ --S -> top2] = e;/*先top2-1后给数组元素赋值*/
return OK;
}
- pop
/*若栈不为空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR*/
Status Pop( SqDoubleStack *S, SElemType *e, int stackNumber)
{
if( stackNumber == 1) /*栈1有元素进栈*/
{
if( S -> top1 == -1)
return ERROR; /* 说明栈1已经是空栈,溢出*/
*e = S -> data[ S -> top1--]; /* 将栈1的栈顶元素出栈*/
}
else if( stackNumber == 2)
{
if( S -> top2 == MAXSIZE)
return ERROR; /* 说明栈2已经是空栈,溢出*/
*e = S -> data[ S -> top2++]; /* 将栈2的栈顶元素出栈*/
}
return OK;
}
2.4栈的应用
-
递归
-
斐波那契数列的实现:
第一个数和第二个数是1,后面的数都是前两个数之和。
可以这样理解:
如果兔子在出生两个月后就有繁殖能力,一对兔子每月生出一对小兔子,假设兔子都不死,每月兔子的对数所组成的集合就是斐波那契数列。
若求解某月兔子的对数,则可以使用for循环迭代或递归。
递归函数的定义:直接调用自己,或通过一系列语句间接调用自己的函数。 -
递归函数的要素:停止递归的条件,函数有返回值。
递归的本质:栈。在前进过程中,对于每一层递归,函数的局部变量,参数值以及返回值都压入栈中。在退回阶段,位于栈顶部的局部变量、参数值和返回值被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。 -
四则运算表达式求值
3.队列
只允许在一端插入,另一端删除的线性表
3.1 循环队列
判断队列空栈还是满栈的条件:
1、设置标志变量flag,当front == rear,flag = 0
时为空,当front == rear,flag = 1
时为满。
2、保留一个元素空间。最大尺寸为QueueSize时,满栈的条件是:
(rear + 1)%QueueSize == front.
计算队列元素个数的通用公式为:(rear - front + QueueSize)%QueueSize
- 实现
/* QElemType类型根据情况而定,这里假设为int*/
Typedef int QElemType;
/* 循环队列的顺序存储结构*/
typedef struct
{
Qelemdef data[MAXSIZE];
int front; /*头指针*/
int rear; /*尾指针,若队列不空,指向队列尾元素的下一个位置*/
}SqQueue;
- 初始化
/* 初始化一个空队列*/
Status InitQueue( SqQeue *Q)
{
Q -> front = 0;
Q -> rear = 0;
return OK;
}
- 求队长
/* 返回Q的元素个数,也就是队列的当前长度*/
int QueueLwngth( SqQeue Q)
{
return (Q.rear - Q.front + MAXSIZE)%MAXSIZE;
}
- 入队
/* 若队列未满,则插入元素e作为Q新的队尾元素*/
Status EnQueue( SqQueue *Q, QElemType e)
{
if( (q -> rear +1)%MAXSIZE == Q -> front); /* 队列满的判断*/
return ERROR;
Q -> data[Q -> rear] = e; /* 将元素e赋值给队尾*/
/* rear指针向后移动一个位置*/
Q -> rear = (Q -> rear + 1)%MAXSIZE;
return OK;
}
- 出队
/* 若队列不空,则删除Q中的队头元素,用e返回其值*/
Status DeQueue( SqQueue *Q, QElemType *e)
{
if( Q -> front == Q -> rear) /* 队列空的判断*/
return ERROR;
*e = Q -> data[Q -> front]; /* 将队头元素赋值给e*/
/* front指针向后移一个位置,若到最后,则转向数组头部*/
Q -> front = (Q -> front +1)%MAXSZIE;
return OK;
}
3.2 链式队列
- 定义
type int QElemType; /* QElemType类型根据实际情况而定,这里假设为int*/
typedef struct QNode /* 结点结构*/
{
QElemType data;
struct QNode *next;
}QNode, *QueuePtr;
typedef struct /* 队列的链表结构*/
{
QueuePtr front, rear; /* 队头,队尾指针*/
}LinkQueue;
- 入队
/* 插入元素e为Q的新队尾元素*/
Status EnQueue(LinkQueue *Q, QElemType e)
{
QueuePtr s = (QueuePtr)malloc( sizeof( QNode));
if(!s) /* 储存分配失败*/
exit(OVERFLOW);
s-> data = e;
s -> next = NULL;
/* 把拥有元素e的新结点s赋值给原队尾结点的后继*/
Q -> rear -> next = s;
Q -> rear = s; /* 把当前的s设置为队尾结点,rear指向s*/
return OK;
}
- 出队
/* 若队列不为空,删除队列的队头元素,用e返回其值,并返回OK,否则返回ERROR*/
Status DeQueue( LinkQueue *Q, QElemType *e)
{
QueuePtr p;
if( Q->front == Q->rear)
return ERROR;
p = Q->data; /* 将欲删除的队头结点暂存给p*/
*e = p->data; /* 将欲删除的队头结点的值赋值给e*/
/* 将原队头结点后继p->next赋值非给头结点的后继*/
Q->front->next = p->next;
if( Q->rear == p) /* 若队头是队尾,则删除后将rear指向头结点*/
Q->rear = Q->front;
free(p);
return OK;
}
4.树和二叉树
是由0个或多个字符组成的有序数列,又名字符串。
4.1 字符串的比较
1、s = "hap", t = "happy"
,则 s<t
2、s = "happen", t = "happy"
, 则 s<t
(e的ACSII是101,y的ACSII是121)
4.2 串的数据结构
操作Index的实现算法
/* T为非空串。若主串S中的第pos个字符之后存在与T相等的子串,*/
/* 则返回第一个这样的子串在S中的位置,否则返回0*/
/* 没有查到哪个语言的字符串关键字是这样的写法*/
/* StringLength方法也不知道是哪种语言求字符串长度的方法*/
/* 内容摘自书中,希望大神解惑*/
int Index( String S, String T, int pos)
{
int n, ,m, i;
String sub;
if( pos > 0)
{
n = StringLength(S); /* 得到主串S的长度*/
m = StringLength(T); /* 得到子串T的长度*/
i = pos;
if( i <= n-m+1)
{
/* 取主串第i个位置长度与T相等的子串给sub*/
SubString(sub, S, i, m);
if( StrCompare( sub, T) != 0) /* 如果两串不相等*/
++i;
else /* 如果两串相等*/
return i; /*返回i的值*/
}
}
return 0; /* 若无子串与它相等,返回0*/
}
4.3 串的存储结构
连接和操作选择链式结构,存储选择顺序结构(存储灵活,性能好)
-
顺序存储
“\0”表示串值的终结连接、插入、替换都有可能超过数组的MAXSIZE
-
链式存储
可以考虑多个字符为一个结点,减少浪费
4.4 朴素的模式匹配算法
子串的定位操作通常称为串的模式匹配。
主串:S = "goodgoogle"中找到子串:T = "google".
就是对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止。
- 实现
/* 假设T[0]和S[0]代表两字符串的长度*/
/* 返回子串T在主串S中第pos个字符之后的位置。
若不存在,则函数返回值为0*/
/* T非空,1 <= pos <= StrLength(S)*/
int Index (String S, String T, int pos)
{
int i = pos; /* i用于主串S中当前位置下标*/
/* 若pos不为1,则从pos位置开始匹配*/
int j = 1; /* j用于子串T中当前位置下标*/
while( i <= S[0] && j <= T[0])
/* 若i小于S的长度,j小于T的长度,则继续*/
{
if( S[i] == T[j]) /* 两字母相等则继续*/
{
++i;
++j;
}
else /* 指针后退,重新开始匹配*/
{
i = i -j + 2; /* i退回到上一次匹配首位的下一位*/
j = 1; /* j退回到子串T的首位*/
}
if( j > T[0])
return i - T[0];
else
return 0;
}
}
4.5 KMP模式匹配算法
朴素算法中存在多余的回返步骤,因此该算法着重改进回返部分。
我们把T串各位置的j值变化定义一个数组next,那么next的长度就是T串的长度,我们可以得到下面函数定义:
next[j] =
0 当j = 1时 Max
{ k | 1<k<j, 且‘P(1)…P(k-1) = P(j+k-1)……P(kj-1)’}当此集合不为空时
1 其他情况时,Max <=>前缀与后缀相等数+1
例:
j = 1 2 3 4 5 6
模式串T a b c a b x
next[j] 0 1 1 1 2 3
j = 1, next[j] = 0;j = 2, 看串‘a’, 该串没有前缀和后缀,故为0+1 = 1;j = 3, 看串'ab', 前缀为‘a’, 后缀为‘b’, 相等数为0,0+1 = 1;j = 4, 看串‘abc’,同为1,j = 5,串‘abca’,相等为1,next[j] = 1+1=2;j = 6,串'abcab', next[j] = 2+1 = 3。
需要注意的是下面这种情况:
j = 1 2 3 4 5 6
模式串T a a a a a x
next[j] 0 1 2 3 4 5
j = 2时,串'aa', 前缀为‘a’, 后缀为'a',如果将整个串都认为是前缀或后缀是不妥当的。
- 算法实现
/* 通过计算返回子串T的next数组*/
void get_next( String T, int *next)
{
int i, j;
i = 1;
j = 0;
next[1] = 0;
while( i < T[0]) /* 此处T[0]表示字符串的长度*/
{
if( j == 0 && T[i] == T[j]) /* T[i]表示后缀的单个字符*/
/* T[j]表示前缀的单个字*/
{
++i;
++j;
next[i] = j;
}
else
j = next[j]; /* 若字符不相同,则j值回溯*/
}
}
/* 假设T[0]和S[0]代表两字符串的长度*/
/* 返回子串T在主串S中第pos个字符之后的位置。
若不存在,则函数返回值为0*/
/* T非空,1 <= pos <= StrLength(S)*/
int Index_KMP (String S, String T, int pos)
{
int i = pos; /* i用于主串S中当前位置下标*/
/* 若pos不为1,则从pos位置开始匹配*/
int j = 1; /* j用于子串T中当前位置下标*/
int next[255]; /* 定义一next数组*/
get_next( T, next); /* 对串T做分析,得到next数组*/
while( i <= S[0] && j <= T[0])
/* 若i小于S的长度,j小于T的长度,则继续*/
{
if( j==0 || S[i] == T[j]) /* 两字母相等则继续*/
{
++i;
++j;
}
else /* 指针后退,重新开始匹配*/
{
j = next[j]; /* j退回到合适位置,i不变*/
}
if( j > T[0])
return i - T[0];
else
return 0;
}
}
- 算法改进
/* 对next数组的改良*/
/* 求模式串T的next函数修正值,并存入数组nextval*/
void get_nextval( String T, int *next)
{
int i, j;
i = 1;
j = 0;
next[1] = 0;
while( i < T[0]) /* 此处T[0]表示字符串的长度*/
{
if( j == 0 && T[i] == T[j]) /* T[i]表示后缀的单个字符*/
/* T[j]表示前缀的单个字符*/
{
++i;
++j;
if( T[i] != T[j])
nextval[i] = j; /* 当前j为nextval在i位置的值*/
else
nextval[i] = nextval[j]; /* 如果与前缀字符相同,
则将前缀字符的nextval值赋给nextval在i位置的值*/
}
else
j = next[j]; /* 若字符不相同,则j值回溯*/
}
}
如有错误,还望指出,转载请注明出处,谢谢。