数据结构线性表初接触
刚刚接触数据结构这学问,通过听课看书自行练习了用C语言实现线性表的基本运算,其中有许多点值得注意。这里存储各个功能分块的代码,以便自身的理解及以后的复习;
线性表(List):零个或多个数据元素的有限序列。
线性表顺序存储结构代码描述
线性表的顺序存储的结构代码:
/* 存储空间初始分配量 */ #define MAXSIZE 20 /* ElemType类型根据实际情况而定,这里假设为int */ typedef int ElemType; typedef struct { /* 数组存储数据元素,最大值为MAXSIZE */ ElemType data[MAXSIZE]; /* 线性表当前长度 */ int length; } SqList;
可以发现描述顺序存储结构需要三个属性:
- 存储空间的起始位置:数组data,它的存储位置就是存储空间的存储位置。
- 线性表的最大存储容量:数组长度MaxSize。
- 线性表的当前长度:length。
对于线性表的顺序存储结构来说,要实现GetElem操作,即将线性表L中的第i个位置元素值返回,就程序而言,只要i的数值在数组下标范围内,就是把数组第i-1下标的值返回即可。来看代码:
#define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status; /* Status是函数的类型,其值是函数结果状态代 码,如OK等 */ /* 初始条件:顺序线性表L已存在,1≤i≤ ListLength(L) */ /* 操作结果:用e返回L中第i个数据元素的值 */ Status GetElem(SqList L, int i, ElemType *e) { if (L.length == 0 || i < 1 || i > L.length) return ERROR; *e = L.data[i - 1]; return OK; }
这里返回值类型Status是一个整型,返回OK代表1,ERROR代表0。之后代码中出现就不再详述。
如果要实现ListIn-sert(*L,i,e),即在线性表L中的第i个位置插入新元素e,
插入算法的思路:
- 如果插入位置不合理,抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或动态增加容量;
- 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置;
- 将要插入元素填入位置i处; ?表长加1。
实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤ ListLength(L), */ /* 操作结果:在L中第i个位置之前插入新的数据元 素e,L的长度加1 */ Status ListInsert(SqList *L, int i, ElemType e) { int k; /* 顺序线性表已经满 */ if (L->length == MAXSIZE) return ERROR; /* 当i不在范围内时 */ if (i < 1 || i >L->length + 1) return ERROR; /* 若插入数据位置不在表尾 */ if (i <= L->length) { /*将要插入位置后数据元素向后移动一位 */ for (k = L->length - 1; k >= i - 1; k--) L->data[k + 1] = L->data[k]; } /* 将新元素插入 */ L->data[i - 1] = e; L->length++; return OK; }
线性表的顺序存储结构删除元素:
删除算法的思路:
- 如果删除位置不合理,抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置;
- 表长减1。
实现代码如下:
/* 初始条件:顺序线性表L已存在,1≤i≤ ListLength(L) */ /* 操作结果:删除L的第i个数据元素,并用e返回 其值,L的长度减1 */ Status ListDelete(SqList *L, int i, ElemType *e) { int k; /* 线性表为空 */ if (L->length == 0) return ERROR; /* 删除位置不正确 */ if (i < 1 || i > L->length) return ERROR; *e = L->data[i - 1]; /* 如果删除不是最后位置 */ if (i < L->length) { /* 将删除位置后继元素前移 */ for (k = i; k < L->length; k++) L->data[k - 1] = L->data[k]; } L->length--; return OK; }
线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1);而插入或删除时,时间复杂度都是O(n)。这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。当然,它的优缺点还不只这些……
线性表链式存储结构代码描述
单链表中,我们在C语言中可用结构指针来描述。
/* 线性表的单链表存储结构 */ typedef struct Node { ElemType data; struct Node *next; } Node; /* 定义LinkList */ typedef struct Node *LinkList;
获得链表第i个数据的算法思路:
1.声明一个指针p指向链表第一个结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3.若到链表末尾p为空,则说明第i个结点不存在;
4.否则查找成功,返回结点p的数据。
实现代码算法如下:
/* 初始条件:顺序线性表L已存在,1≤i≤ ListLength(L) */ /* 操作结果:用e返回L中第i个数据元素的值 */ Status GetElem(LinkList L, int i, ElemType *e) { int j; LinkList p; /* 声明一指针p */ p = L->next; /* 让p指向链表L的第个结点 */ j = 1; /* j为计数器 */ /* p不为空且计数器j还没有等于i时,循环继续 */ while (p && j < i) { p = p->next; /* 让p指向下一个结点 */ ++j; } if (!p || j > i) return ERROR; /* 第i个结点不存在 */ *e = p->data; /* 取第i个结点的数据 */ return OK; }
单链表第i个数据插入结点的算法思路:
1.声明一指针p指向链表头结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
3.若到链表末尾p为空,则说明第i个结点不存在;
4.否则查找成功,在系统中生成一个空结点s;
5.将数据元素e赋值给s->data;
6.单链表的插入标准语句s->next=p->next;p->next=s;
7.返回成功。
实现代码算法如下:
/* 初始条件:顺序线性表L已存在,1≤i≤ ListLength(L), */ /* 操作结果:在L中第i个结点位置之前插入新的数 据元素e,L的长度加1 */ Status ListInsert(LinkList *L, int i, ElemType e) { int j; LinkList p, s; p = *L; j = 1; /* 寻找第i-1个结点 */ while (p && j < i) { p = p->next; ++j; } /* 第i个结点不存在 */ if (!p || j > i) return ERROR; /* 生成新结点(C标准函数) */ s = (LinkList)malloc(sizeof(Node)); s->data = e; /* 将p的后继结点赋值给s的后继 */ s->next = p->next; /* 将s赋值给p的后继 */ p->next = s; return OK; }
单链表第i个数据删除结点的算法思路:
1.声明一指针p指向链表头结点,初始化j从1开始;
2.当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
3.若到链表末尾p为空,则说明第i个结点不存在;
4.否则查找成功,将欲删除的结点p->next赋值给q;
5.单链表的删除标准语句p->next=q->next;
6.将q结点中的数据赋值给e,作为返回;
7.释放q结点;
8.返回成功。
实现代码算法如下:
/* 初始条件:顺序线性表L已存在,1≤i≤ ListLength(L) */ /* 操作结果:删除L的第i个结点,并用e返回其 值,L的长度减1 */ Status ListDelete(LinkList *L, int i, ElemType *e) { int j; LinkList p, q; p = *L; j = 1; /* 遍历寻找第i-1个结点 */ while (p->next && j < i) { p = p->next; ++j; } /* 第i个结点不存在 */ if (!(p->next) || j > i) return ERROR; q = p->next; /* 将q的后继赋值给p的后继 */ p->next = q->next; /* 将q结点中的数据给e */ *e = q->data; /* 让系统回收此结点,释放内存 */ free(q); return OK; }
回顾一下,顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。而单链表和顺序存储结构就不一样,它不像顺序存储结构这么集中,它可以很散,是一种动态结构。对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。
所以创建单链表的过程就是一个动态生成链表的过程。即从“空表”的初始状态起,依次建立各元素结点,并逐个插入链表。
单链表整表创建的算法思路:
1.声明一指针p和计数器变量i;
2.初始化一空链表L;
3.让L的头结点的指针指向NULL,即建立一个带头结点的单链表;
4.循环:
- 生成一新结点赋值给p;
- 随机生成一数字赋值给p的数据域p->data;
- 将p插入到头结点与前一新结点之间。
实现代码算法如下:
头插法:始终让新结点在第一的位置。
/* 随机产生n个元素的值,建立带表头结点的单链 线性表L(头插法) */ void CreateListHead(LinkList *L, int n) { LinkList p; int i; /* 初始化随机数种子 */ srand(time(0)); *L = (LinkList)malloc(sizeof(Node)); /* 先建立一个带头结点的单链表 */ (*L)->next = NULL; for (i = 0; i < n; i++) { /* 生成新结点 */ p = (LinkList)malloc(sizeof(Node)); /* 随机生成100以内的数字 */ p->data = rand() % 100 + 1; p->next = (*L)->next; /* 插入到表头 */ (*L)->next = p; } }
尾插法:把每次新结点都插在终端结点的后面。
CreateListTail单链表整表删除的算法思路如下:
1.声明一指针p和q;
2.将第一个结点赋值给p;
3.循环:
- 将下一结点赋值给q;
- 释放p;
- 将q赋值给p。
实现代码算法如下:
/* 初始条件:顺序线性表L已存在,操作结果:将L 重置为空表 */ Status ClearList(LinkList *L) { LinkList p, q; /* p指向第一个结点 */ p = (*L)->next; /* 没到表尾 */ while (p) { q = p->next; free(p); p=q; } /* 头结点指针域为空 */ (*L)->next = NULL; return OK; }
通过上面的对比,我们可以得出一些经验性的结论:
- 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构。比如说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构。而游戏中的玩家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不太合适了,单链表结构就可以大展拳脚。当然,这只是简单的类比,现实中的软件开发,要考虑的问题会复杂得多。
- 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。
总之,线性表的顺序存储结构和单链表结构各有其优缺点,不能简单的说哪个好,哪个不好,需要根据实际情况,来综合平衡采用哪种数据结构更能满足和达到需求和性能。
而另外,还有静态链表和循环链表,双向链表等都可像上面的基本操作上推导出来。

浙公网安备 33010602011771号