数据结构复习(二)线性结构(1)

定义:

线性结构即为数据元素之间构成了一个有序的序列。线性结构拥有逻辑上的序列,而不一定是物理上的。比方说我们在现实生活中排队,那么队列就是一个线性结构,有前后之分。倘若我们在网络上排队(例如某游戏排队进入服务器)虽然我们在物理上并没有紧紧相挨,但是依旧在逻辑上属于有序的序列,故也是线性结构。线性结构是一个一维的结构,具有“一对一”关系。

最简单的线性结构——数组

数组毫无疑问是一个线性结构,且十分简单。由于在之前已经介绍过了,所以此处省略她。

进化后的线性结构——线性表

数组元素是在物理上相互连接的,但是在线性表中,就不一定如此。根据线性表的数据存储方式,我们可以将线性表分为顺序表链表,分别对应了顺序存储结构和链式存储结构。

数组的进化——顺序表

顺序表本质上来看还是通过数组实现的线性存储结构,我们一起来瞅一眼定义。

#define MAXSIZE 1000
// 定义MAXSIZE为1000
typedef int ElementType;
typedef int Position;
// 用户自定义的类型Position,在代码中表示"位置",即为数组的下标,从0开始。
typedef struct LNode * PtrToLNode;
// 定义了一个叫做PtrToLNode的玩意,用于替换LNode结构体的指针类型
// 所以接下来的PtrToLNode是个指针类型哈
struct LNode{
	ElementType Data[MAXSIZE];
	// 用于存放数据的数组 
	Position Last;
	// 指示最后一个数据的下标 
};
typedef PtrToLNode List;
// 再给PtrToLNode换个名字叫List,估计是原来那个太长了吧。 

这边咱们定义了一个结构体(就是传说中的顺序表)看起来似乎有一丢丢复杂,其实非常简单。就是把一个数组和一个下标组合了起来,下标从0开始,跟数组并没有多大差别。

接下来我们看看顺序表的初始化,要用到之前介绍的malloc函数。

List MakeEmpty(){
	List L;
	L=(List)malloc(sizeof(struct LNode));
	L->Last=-1;
	return L;
}

接下来咱们玩点好玩的,大家想想这个诡异的结构怎么插入数据x到位置i上呢。

首先我们肯定要判断位置i是不是合法的,我们知道i肯定不能小于1,因为i不同于下标,第0位没有意义,不能向0位插入内容。同时,i也要小于Last值+2,以此保证顺序表内的数据是连续的。同时因为Last的值是目前最后一个元素的下标,如果i=Last+1的话,即为往最后插入。(这里如果现在不理解,可以等看完下面的插入逻辑再回头来看)

所以,我们能够得到第一个判断。

if(i<1||i>L->Last+2){
	printf("位序不合法");
	return false;
}

同时我们也需要判断顺序表是否已经满了,如果满了肯定不能进行插入。此时Last的值必须小于数组容量最大值-1,因为Last是下标,从0开始。

if(L->Last==MAXSIZE-1){
	printf("表满");
	return false;
}

所以我们来看看插入函数的逻辑是什么。首先我们从插入位序i所储存的元素开始,将后面的元素向后移动一格。

 

然后我们将位置为2的储存空间赋值x并且让最后一位的下标Last++即可。

接下来我们来看看这一步的代码细节。

我们需要从后往前去进行位移,因为如果从前往后就会导致后面的数据被前面的所覆盖。

 

Position j;
// 这里j是下标,下一行j>=i-1是因为i是位序,需要-1转换成数组下标 
for(j=L->Last;j>=i-1;j--){
	L->Data[j+1]=L->Data[j];
	// 将当前下标的数组的值赋值给数组下一个位置 
}
L->Data[i-1]=x;
L->Last++;

这里给出完整的代码

bool Insert(List L, ElementType x, int i){
	if(i<1||i>L->Last+2){
		printf("位序不合法");
		return false;
	}
	if(L->Last==MAXSIZE-1){
		printf("表满");
		return false;
	}
	Position j;
	// 这里j是下标,下一行j>=i-1是因为i是位序,需要-1转换成数组下标 
	for(j=L->Last;j>=i-1;j--){
		L->Data[j+1]=L->Data[j];
		// 将当前下标的数组的值赋值给数组下一个位置 
	}
	L->Data[i-1]=x;
	L->Last++;
	return true;
}

当插入过程了然于心后,删除某个位置的元素也变得简单了起来。我们只要判断删除的那个位置是否合法,然后将后面的元素前移,最后将Last--即可。废话不多说,上代码。

bool Delete(List L, int i){
	if(i<1||i>L->Last+1){
		printf("位序%d不存在元素", i);
		return false;
	}
	Position j;
	for(j=i-1;j<=L->Last;j++){
		L->Data[j]=L->Data[j+1];
	}
	L->Last--;
	return true;
}

最后我们来瞅一眼查找函数。此函数需要我们找到顺序表中第一个值为x的元素并且输出它的下标。

其实这个函数非常简单,我们只要遍历整个顺序表,找到元素的时候停止遍历然后输出下标即可。如果遍历完成但是没找到元素,就输出错误码。

Position Find(List L, ElementType x){
	Position i=0;
	while(i<=L->Last && L->Data[i]!=x){
		// i从0开始,一直到Last结束 
		i++;
	}
	// 如果没有找到x的话,i=Last+1 
	if(i>L->Last){
		// 通过这个判断x是否有被找到 
		return -1;
	}else{
		return i;
	}
}

冰糖葫芦——链表

链表长得就像不那么直的冰糖葫芦。她在内存中并不是连续的,像是一颗颗珍珠,然后用指针相连。一个链表节点分为两个部分:数据域指针域

顾名思义,在数据域内,我们可以存放数据,指针域则存放指针。

链表长得像这个样子:

所以,链表的结构定义就非常简单了,一个结构体,两部分组成,一个成员变量是Data,一个是指针。

typedef int ElementType;
typedef struct LNode * PtrToLNode;
struct LNode{
	ElementType Data;
	PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;

链表的每一个节点都需要我们去malloc出来,然后将指针指向已有的节点。所以就出现了头插法和尾插法。头插法是将新的节点指针指向原来的一条链表的第一个节点,尾插法自然就是原链表最后一个节点的指针指向新节点。

头插法:

List InsertH(List L, ElementType x){
	Position tmp;
	// 新建一个节点的指针变量 
	tmp=(List)malloc(sizeof(struct LNode));
	// 为其分配空间 
	tmp->Data=x;
	// 填充数据域 
	tmp->Next=L;
	// 填充指针域 
	return tmp;
}

尾插法跟接下来要展示的插入差不多,所以不做演示了,请各位看官继续看。

事实上,链表也有几种分类。最简单的是带头结点和不带头结点的链表。头结点是一个固定的节点,代表了链表的开头。它的数据域不存放数据,指针域指向链表的第一个节点。这里我们以带头结点的链表为例,展示一下链表的插入函数。

首先来分析一下,往链表某位置插入元素非常简单。我们需要链表的头结点L,需要插入的元素x,以及插入的位序i。当然,我们需要留意i的合法性,即i这个位置可能不在链表内。然后我们找到此位置,并且将前面一个链表的指针域指向新节点,将新节点的指针域指向原本在此位置上的节点。听起来有点绕,上图!

 

画图水平很差,请各位见谅。蓝色连线会被取消,取而代之的是红色的连线。

话不多说,我们来瞅瞅代码。

bool Insert(List L, ElementType x, int i){
	Position tmp, pre;
	// tmp保存新的节点,pre保存前一个节点。
	int cnt=0;
	// 计数器
	pre=L;
	// 从头开始 
	while(pre && cnt=i-1){
		pre=pre->Next;
		// 将pre更新为下一个节点 
		cnt++;
		// 计数器+1 
	}
	if(pre == NULL || cnt!=i-1){
		printf("位置错误");
		return false;
	}else{
		tmp=(List)malloc(sizeof(struct LNode));
		// 分配空间 
		tmp->Data=x;
		// 填充数据域 
		tmp->Next=pre->Next;
		// 新节点的指针指向前一个节点的指针所指向的地方(即为后一个节点)
		pre->Next=tmp;
		// 将前一个节点的指针指向新节点
		return true; 
	}
}

领悟了插入,那么删除也就十分简单了。我们只需要找到i位序对应的节点b,将前一个节点a的指针指向b指针域所指向的节点c,然后free(b)将b节点的空间释放就行了。代码跟插入基本一致。

bool Insert(List L, ElementType x, int i){
	Position tmp, pre;
	// tmp保存要删除的节点,pre保存前一个节点。
	int cnt=0;
	// 计数器
	pre=L;
	// 从头开始 
	while(pre && cnt=i-1){
		pre=pre->Next;
		// 将pre更新为下一个节点 
		cnt++;
		// 计数器+1 
	}
	if(pre == NULL || cnt!=i-1 || pre->Next==NULL){
		// 注意这里多了一个条件,因为插入可以向最后插,但是删除不能删除一个不存在的东西。 
		printf("位置错误");
		return false;
	}else{
		tmp=pre->Next;
		// 将tmp赋值为要删除的节点 
		pre->Next=tmp->Next;
		// 将前一个节点的指针指向要删除节点的后一个节点 
		free(tmp);
		// 她自由了 
		return true; 
	}
}

领悟了插入,让我们将插入的过程再次拆解,就能获得两个新的函数。分别是插入的前段——求链表长度;和插入的后段——找链表内元素。

求表长:

int Length(List L){
	Position tmp;
	int cnt=0;
	tmp=L;
	while(tmp){
		tmp=tmp->Next;
		cnt++;
	}
	return cnt;
}

按位序查找元素:

ElementType FindKth(List L, int i){
	Position tmp;
	int cnt=0;
	tmp=L;
	while(tmp){
		tmp=tmp->Next;
		cnt++;
	}
	if((cnt==i)&&tmp){
		return tmp->Data;
	}else{
		printf("位置错误");
		return -1;
	}
}

我们如果在遍历链表的过程内插入一个判断,就可以获得查找元素值的函数:

int Find(List L, ElementType x){
	Position tmp;
	tmp=L;
	int cnt=0;
	while(tmp){
		// 开始遍历 
		tmp=tmp->Next;
		cnt++;
		if(tmp->Data=x){
			// 如果找到了就返回 
			return cnt;
		}
	}
	return -1;
	// 遍历完成都没有找到,返回-1 
}
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值