【游戏开发笔记】编程篇_数据结构

@

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值回溯*/
	}
}

如有错误,还望指出,转载请注明出处,谢谢。

posted @ 2021-12-31 10:35  码农要战斗  阅读(68)  评论(0编辑  收藏  举报