线性表的链式存储结构
顺序存储结构不足的解决办法
从上面可以看出,线性表的顺序存储结构,最大的缺点就是插入和移除时需要移动大量元素,这显然就需要耗费时间。链式存储结构就是为了弥补顺序存储结构在效率上的问题。
线性表链式存储结构
1. 线性表链式存储结构的定义
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。
为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。
n个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,...,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正式通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起,
示意图:
对于线性表来说,总得有个头有个尾,链表也不例外,我们把链表中第一个结点的存储位置叫做头指针。最后一个结点指针为“空”(通常用NULL或“^”符号表示)。
线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示),示意图:
有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点可以不存储任何信息,也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
示意图:
2. 头指针域、头结点的区别
1)是否为必要元素:头指针是链表的必要元素,无论链表是否为空,头指针均不为空。头结点不一定是链表必须元素。
2)作用方面:头指针是指向链表第一个结点的指针,若链表有头结点,则是指向头结点的指针。而且其具有标识作用,常用头指针冠以链表的名字。而头结点主要是为了操作的统一和方便而设立的,比如对第一元素结点前插入结点和
删除第一结点,其操作就和其它结点的操作统一了。
3. 线性表链式存储结构代码描述
若线性表为空表,则头结点的指针域为“空”,示意图:
从这个结构定义中,我们也就知道,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。
单链表操作
1. 单链表的读取
在单链表中,由于第 i 个元素到底在哪?没办法一开始就知道,必须得从头开始找。因此,对于单链表实现获取第 i 个元素的数据的操作GetElem,在算法上,相对要麻烦一些。
获得链表第 i 个数据的算法思路:
1) 声明一个指针 p 指向链表第一个结点,初始化 j 从1开始
2) 当 j<i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1
3) 若到链表末尾 p 为空,则说明第 i 个结点不存在
4) 否则查找成功,返回结点 p 的数据
C语言的实现:
1 #define OK 1 2 #define ERROR 0 3 #define TRUE 1 4 #define FALSE 5 typedef int Status; 6 /*Status是函数的类型,其值是函数结果状态代码,如OK等*/ 7 /*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/ 8 /*操作结果:用e返回L中第i个数据元素的值*/ 9 10 /*单链表的读取*/ 11 12 Status GetElem(LinkList L, int i, ElemType *e) 13 { 14 int j; 15 LinkList p; /*声明一指针p*/ 16 p = L->next; /*让p指向链表L的第一个结点*/ 17 j = 1; /*当前位置计数器设置为1*/ 18 while (p && j < i) /*p不为空且计数器j还没有等于i时,循环继续*/ 19 { 20 p = p->next; /*让p指向下一个结点*/ 21 ++j; 22 } 23 if (!p || j > i) 24 return ERROR; /*到达结尾且没找到:第i个结点不存在*/ 25 *e = p->data; /*取第i个结点的数据*/ 26 return OK; 27 }
PHP的实现:
1 <?php 2 /** 3 * 读取链表中第i个数据 4 * @param $list object 待插入的链表 5 * @param $i int 节点序号 6 */ 7 8 public function readIThNode($list,$i){ 9 // 如果链表为空或者i小等于0 10 if($list == null || $i<=0){ 11 echo "输入参数不合法"; 12 exit; 13 } 14 // 第1步 15 $p = $list->next; // 设置p指向第一个节点(即头节点的后继节点)) 16 $j=1; // 计时器必须初始化 17 while($p && $j<$i ){ 18 $p = $p->next; 19 ++$j; 20 } 21 22 // 第i步 23 if($p == null || $j>$i){ // 说明链表已经结束,不存在i节点,过滤掉i大于链表长度的情况(因为节点是散列的,事先并不知道其长度) 24 echo "i长度大于链表长度" ; 25 exit; 26 }else{ 27 $e = $p->data; // 第i个节点存在,返回 28 return $e; 29 } 30 31 }
过程分析:
1)其实就是从头开始找,直到找到第 i 个结点为止。这个算法时间复杂度取决于i的位置。当i=1时,不需要遍历,第一个就取出数据了,而当i=n时,则遍历n-1次才可以。因此最坏情况的时间复杂度是O(n)。
2)由于单链表的结构没有定义表长,所以不能事先知道要循环多少次,因此也就不方便使用for来控制循环。其核心思想就是“工作指针后移”,这其实也是很多算法的常用技术。
2. 单链表的插入
单链表第 i 个数据插入结点的算法思路:
1)声明一指针 p 指向链表头结点,初始化 j 从1开始
2)当 j<i 时,就遍历链表,让 p 的指针向后移动,不断指向下一结点,j 累加 1
3)若到链表末尾为空,则说明第 i 个结点不存在
4)否则查找成功,在系统中生成一个空节点s
5)将数据元素 e 赋值给 s->data
6)单链表的插入标准语句 s->next=p->next; p->next=s;
7)返回成功
C语言的实现:
1 #define OK 1 2 #define ERROR 0 3 #define TRUE 1 4 #define FALSE 5 typedef int Status; 6 /*Status是函数的类型,其值是函数结果状态代码,如OK等*/ 7 /*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/ 8 /*操作结果:在L中第i个结点位置之前插入新的数据元素e,L的长度加1*/ 9 10 /*单链表的插入*/ 11 Status ListInsert(LinkList *L, int i, ElemType e) 12 { 13 int j; 14 LinkList p, s; 15 p = *L; 16 j = 1; 17 while (p && j < i) /*寻找第i-1个结点*/ 18 { 19 p = p->next; 20 ++j; 21 } 22 if (!p || j > i) 23 return ERROR; /*第i个结点不存在*/ 24 s = (LinkList)malloc(sizeof(Node)); /*生成新结点(C标准函数)*/ 25 s->data = e; 26 s->next = p->next; /*将p的后继结点赋值给s的后继*/ 27 p->next = s; /*将s赋值给p的后继*/ 28 return OK; 29 }
PHP的实现:
1 /** 2 * 在链表的第i个位置之前插入节点e 3 * @param $list object 待插入的链表 4 * @param $i int 节点序号 5 * @param $e object 待插入的节点 6 */ 7 public function Insert($list, $i, $e) 8 { 9 if ($e == null) { 10 echo "待插入节点为空"; 11 exit; 12 } 13 $p = $this->head; #设置p指向头节点 14 $j = 1; #计时器必须初始化 15 16 while ($p && $j < $i) { 17 $p = $p->next; #保证节点在向后移动 18 ++$j; 19 } 20 21 //第i步 22 if ($p == null || $j > $i) { #说明链表已经结束,不存在i节点,过滤掉i大于链表长度的情况(因为节点是散列的,事先并不知道其长度) 23 echo "不存在i节点"; 24 exit; 25 } else { 26 /* 标准的插入语句(头插法) */ 27 $e->next = $p->next; 28 $p->next = $e; 29 return $list; 30 } 31 }
疑问:
不知道大家有没有发现,在单链表读取的时候,声明的指针 p 指向链表第一个结点,而在单链表插入的时候,声明的指针p指向链表的头结点?
分析:
1)单链表读取的时候,是从头开始找,如果找到直接取出数据(p->data)返回,显然这个跟头结点没什么关系,直接从第一结点开始即可
2)单链表插入的时候,查找到结点的情况下,是需要将 p 的后继结点 改成 s 的后继结点,再把结点 s 变成 p 的后继结点。声明 p 指针指向链表头结点,p的后继结点就是第一结点,就相当于从第一结点前插入,这显然符合当前要求。
如果声明的指针 p 指向第一结点,那通过这个插入语句后,就相当于插入了第二结点前,显然不符合要求。
3. 单链表的删除
删除结点的算法思路:
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) 返回成功
C语言的实现:
1 #define OK 1 2 #define ERROR 0 3 #define TRUE 1 4 #define FALSE 5 typedef int Status; 6 /*Status是函数的类型,其值是函数结果状态代码,如OK等*/ 7 /*初始条件:顺序线性表L已存在,1<=i<=ListLength(L)*/ 8 /*操作结果:删除L的第i个结点,并用e返回其值,L的长度减1*/ 9 10 /*单链表的删除*/ 11 12 Status ListDelete(LinkList *L, int i, ElemType *e) 13 { 14 int j; 15 LinkList p, q; 16 p = *L; 17 j = 1; 18 while (p->next && j < 1) /*遍历寻找第i-1个结点*/ 19 { 20 p = p->next; 21 ++j; 22 } 23 24 if (!(p->next) && j < i) 25 return ERROR; /*第i个结点不存在*/ 26 q = p->next; 27 p->next = q->next; /*将q的后继赋值给p的后继*/ 28 *e = q->data; /*将q结点中的数据给e*/ 29 free(q); /*让系统回收此结点,释放内存*/ 30 return OK; 31 }
PHP的实现:
1 <?php 2 3 /** 4 * 删除链表的第i个节点,并返回该节点的值 5 * @param $list object 待插入的链表 6 * @param $i int 节点序号 7 */ 8 public function Delete($list, $i) 9 { 10 if ($list == null || $i <= 0) { 11 echo "输入参数不合法"; 12 exit; 13 } 14 $p = $this->head; #设置p指向头结点 15 $j = 1; #计时器必须初始化 16 17 while ($p && $j < $i) { 18 $p = $p->next; #保证节点在向后移动 19 ++$j; 20 } 21 22 //第i步 23 if ($p == null || $j > $i) { #说明链表已经结束,不存在i节点,过滤掉i大于链表长度的情况,以为若i大于链表长度,则上面循环会跳出直接进入判断然后返回 24 echo "不存在i节点"; 25 exit; 26 } else { 27 /* 标准的删除语句 */ 28 $q = $p->next; 29 $p->next = $q->next; 30 $e = $q->data; 31 unset($q); 32 return $e; 33 } 34 }
从上面的代码实现来看,我们发现,单链表插入和删除算法,其实都是由两部分组成:第一部分就是遍历查找第 i 个结点,第二部分就是插入和删除结点
总结:从整个算法来说,我们很容易推导出:它们的时间复杂度都是O(n)。如果在我们不知道第 i 个结点的指针位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构是没有太大优势的。但如果,我们希望从第 i 个位置,插入10个结点,对于顺序存储结构意味着,每一次插入都需要移动 n-i 个结点。每次都是 O(n)。而单链表,我们只需要在第一次时,找到第 i 个位置的指针,此时为O(n),接下来只是简单得通过赋值移动指针而已,时间复杂度都是O(1)。对于插入和删除数据越频繁的操作,单链表的效率优势就越是明显。
下面附上:PHP中对单链表的实现
1 <?php 2 // 节点类 3 class Node { 4 public $data; // 节点数据 5 public $next; // 下一节点 6 7 public function __construct($data) { 8 $this->data = $data; 9 $this->next = NULL; 10 } 11 } 12 // 单链表类 13 class SingleLinkedList { 14 private $header; // 头节点 15 16 function __construct($data) { 17 $this->header = new Node($data); 18 } 19 // 查找节点 20 public function find($item) { 21 $current = $this->header; 22 while ($current->data != $item) { 23 $current = $current->next; 24 } 25 return $current; 26 } 27 // (在节点后)插入新节点 28 public function insert($item, $new) { 29 $newNode = new Node($new); 30 $current = $this->find($item); 31 $newNode->next = $current->next; 32 $current->next = $newNode; 33 return true; 34 } 35 36 // 更新节点 37 public function update($old, $new) { 38 $current = $this->header; 39 if ($current->next == null) { 40 echo "链表为空!"; 41 return; 42 } 43 while ($current->next != null) { 44 if ($current->data == $old) { 45 break; 46 } 47 $current = $current->next; 48 } 49 return $current->data = $new; 50 } 51 52 // 查找待删除节点的前一个节点 53 public function findPrevious($item) { 54 $current = $this->header; 55 while ($current->next != null && $current->next->data != $item) { 56 $current = $current->next; 57 } 58 return $current; 59 } 60 61 // 从链表中删除一个节点 62 public function delete($item) { 63 $previous = $this->findPrevious($item); 64 if ($previous->next != null) { 65 $previous->next = $previous->next->next; 66 } 67 } 68 69 // findPrevious和delete的整合 70 public function remove($item) { 71 $current = $this->header; 72 while ($current->next != null && $current->next->data != $item) { 73 $current = $current->next; 74 } 75 if ($current->next != null) { 76 $current->next = $current->next->next; 77 } 78 } 79 80 // 清空链表 81 public function clear() { 82 $this->header = null; 83 } 84 85 // 显示链表中的元素 86 public function display() { 87 $current = $this->header; 88 if ($current->next == null) { 89 echo "链表为空!"; 90 return; 91 } 92 while ($current->next != null) { 93 echo $current->next->data . " "; 94 $current = $current->next; 95 } 96 } 97 }
参考资料: 程杰《大话数据结构》