大话数据结构学习之(二) 线性表

来源:互联网 发布:软件行业税负 编辑:程序博客网 时间:2024/06/05 23:57

1. 线性表的顺序存储结构


用一段连续的存储单元一次存储线性表的数据元素。


结构代码:

const int MAXSIZE = 20; // 存储空间初始分配量typedef int ElemType; // ElemType类型根据实际情况而定,这里假设为inttypedef struct{ElemType data[MAXSIZE]; // 数组存储数据元素int length; // 线性表当前长度}SqList;

这里,我们发现描述顺序存储结构需要三个属性:

  • 存储空间的起始位置:数组data。
  • 线性表的最大存储容量:数组长度MAXSIZE。
  • 线性表的长度:length。

1.1 数组长度与线性表长度的区别:

数组长度是存放线性表存储空间的长度,线性表长度是线性表中数据元素的个数。任意时刻线性表的长度应小于等于数组长度。


其存取性能为O(1),通常把具有这一特点的存储结构成为随机存储结构

1.2 顺序存储结构的插入与删除

1.2.1 插入操作:

// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)// 操作结果:在L中第i个位置之前插入新的数据元素e,L长度加1Status ListInsert(SqList *L, int i, ElemType e){int k;if (L->length == MAXSIZE) // 顺序线性表已满return ERROR;if (i<1 || i>L->length+1) // 当i不在范围内时,注意是length+1return 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.2.2 删除操作:

// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)// 操作结果:删除在L中第i个位置之前的数据元素e,L长度减1Status ListDelete(SqList *L, int i, ElemType *e){int k;if (L->length == 0) // 顺序线性表为空return ERROR;if (i<1 || i>L->length) // 当i不在范围内return ERROR;if (i<L->length) // 当删除位置不在表尾{for (k=i-1; k<L->length; k++) // 将要删除位置后数据元素前移一位{L->data[k] = L->data[k+1];}}L->length--;return Ok;}

复杂度
最好情况O(1),最差情况O(n),平均复杂度为O((n-1)/2) = O(n)。
这说明,它比较适合于元素个数不太变化,而更多是存取数据的应用。

1.3 线性表顺序存储结构的优缺点:

优点:
  • 无需为表中元素之间的逻辑关系而增加额外的存储空间
  • 可以快速存取表中任意位置的元素
缺点:
  • 插入删除操作需要移动大量元素
  • 当线性表长度变化较大时,难以确定存储空间容量
  • 造成存储空间“碎片”


2. 线性表的链式存储结构


链表中第一个结点的存储位置叫做头指针。线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)。
有时,我们为了更加方便的对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表长度等附加信息,头结点的指针域存储指向第一个结点的指针。



头结点与头指针的异同:

头指针:
  • 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
  • 头指针具有标识作用,所以常用头指针冠以链表的名字
  • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素
头结点:
  • 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般没有意义(也可以放链表长度)
  • 有了头结点,对在第一个结点前插入和删除第一个结点,其操作与其他结点的操作就统一了
  • 头结点不一定是链表的必要元素

// 线性表的单链表存储结构typedef struct Node{ElemType data;struct Node *next;}Node;typedef struct Node *LinkList; // 定义LinkList


2.1 单链表的读取


// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)// 操作结果:用e返回L中第i个数据元素的值Status GetElem(LinkList L, int i, ElemType *e){int j;LinkList p;p = L->next; // 让p指向链表L的第一个结点j = 1;while(p && j<i){p = p->next;++j;}if (!p || j>i){return ERROR; // 第i个元素不存在}*e = p->data; // d取第i个元素的数据return Ok;}

说白了,就是从头开始找,直到第i个元素为止。时间复杂度取决于i的位置,i=1时不需要遍历,而i=n时,则遍历n-1次才可以。


2.2 单链表的插入与删除


2.2.1 单链表的插入


// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)// 操作结果:在L中第i个位置之前插入新的数据元素e,L长度加1Status ListInsert(LinkList *L, int i, ElemType e){int j;LinkList p,s;p = *L;j = 1;while(p && j<i){p = p->next;++j;}if (!p || j>i){return ERROR; // 第i个元素不存在}s = (LinkList)malloc(sizeof(Node));// 生成新结点s->data = e;s->next = p->next;// 将p的后继结点赋值给s的后继p->next = s; // 将s赋值给p的后继return Ok;}
对于单链表的表头和表尾的特殊情况,操作是相同的。

2.2.2 单链表的删除


// 初始条件:顺序线性表L已经存在,1<=i<=ListLength(L)// 操作结果:删除在L中第i个位置之前的数据元素e,L长度减1Status ListDelete(LinkList *L, int i, ElemType *e){int j;LinkList p,q;p = *L;j = 1;while(p->next && j<i){p = p->next;++j;}if (!(p->next) || j>i){return ERROR; // 第i个元素不存在}q = p->next;p->next = q->next; *e = q->data; // 将q结点中的数据给efree(q); // 回收此结点return Ok;}


对插入删除数据越频繁的操作,单链表的效率优势就越明显。


2.3 单链表的整表创建


创建单链表的过程就是一个动态生成链表的过程,即从“空表”的初始状态起,依次建立各个元素结点,并逐个插入链表。

2.3.1 头插法



// 随机产生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)); // 生成新结点p->data = rand()%100+1;// 随机生成100内数字p->next = (*L)->next;(*L)->next = p;// 插入到表头}}


2.3.2 尾插法


// 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法)void CreateListTail(LinkList *L, int n){LinkList p,r;int i;srand(time(0)); // 初始化随机数种子*L = (LinkList)malloc(sizeof(Node)); r = *L; // r为指向尾部的结点for (i=0; i<n; i++){p = (LinkList)malloc(sizeof(Node)); // 生成新结点p->data = rand()%100+1;// 随机生成100内数字r->next = p;// 将表尾结点的指针指向新结点r = p;// 将当前的新结点定义为表尾结点}r->next = NULL;}


2.4 单链表的整表删除


// 初始条件:顺序线性表L已经存在// 操作结果:将L重置为空表Status ClearList(LinkList *L){LinkList p,q;p = (*L)->next; // p指向第一个结点while(p){q = p->next;free(p);p = q;}(*L)->next = NULL;// 头结点指针域为空return Ok;}

要知道p是一个结点,除了数据域还有指针域,free(p)是对整个结点进行删除和内存释放工作。所以如果程序中直接像下面这样写会出问题的(p的地址域已经被释放)
free(p);
p = p->next;

2.5 单链表结构与顺序存储结构的比较


  • 频繁查找,很少进行插入删除操作,宜采用顺序存储结构;反之用单链表。比如游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应考虑用顺序存储结构。而游戏中玩家的武器装备列表,随着玩家的游戏过程中,可能随时增加或删除,单链表就可以大展拳脚了。
  • 当线性表中元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样就不需要考虑存储空间大小问题。若事先知道大致长度,如一年12个月,这种用顺序结构效率会高很多。

2.6 循环链表


对于非空的循环链表如下:

其实循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p->next是否为空,现在是它不等于头结点,则循环结束。

2.7 双向链表



双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。

// 线性表的双向链表存储结构typedef struct DulNode{ElemType data;struct DulNode *prior; // 直接前驱指针struct DulNode *next; // 直接后继指针}DulNode, *DuLinkList;

对于双向链表中的一个结点p,它的后继的前驱,以及前驱的后继都是自己。

2.7.1 插入操作


插入操作时,并不复杂,但是顺序很重要,不能写反了。




s->prior = p; // 把p赋值给s的前驱,如图中的1s->next = p->next; // 把p->next赋值给s的后继,如图中2p->next->prior = s; // 把s赋值给p->next的前驱,如图中3p->next = s;       // 把s赋值给p的后继,如图中的4

2.7.2 删除操作





p->prior->next = p->next; p->next->prior = p->prior;free(p);




0 0
原创粉丝点击