本篇文章紧接上篇文章,上文了解了线性表的顺序存储结构,本篇文章我们来了解线性表的链式存储结构,并对比两种存储结构的优缺点。
顺序存储结构虽然有它的优点,但是,顺序存储结构也有一个很大的缺陷,它的插入和删除需要挪动大量的元素,造成算法效率的问题,现在存储的数据,进行的操作还比较少,所以感受不到,但是当操作数量达到一定程度的时候,用顺序存储结构写的算法的不足就体现出来了。
上面顺序存储结构所说的不足,由此,我们需要链式存储结构来解决这个问题
我们通过一个例子来了解链式存储结构与顺序存储结构的区别
例1
假设现在有两家动物园,两家动物园为了游客能够观看到所有动物,以及方便游客进行寻找,A动物园将每种动物观赏地放在一条线路上,每一种动物前后都有上一种动物和下一种动物;B动物园将每种动物放在了最适合的位置,散落在动物园各处,为了方便游客,在每种动物的观赏地会放上一块牌子,推荐游客去往下一个地方,以便游客看完所有动物。现在,动物园要新加一种动物,A动物园想把它放到中间,但是没有位置了,必须将后半部分的动物向后挪一个位置才行;而B动物园只需要更换一块牌子,将上一个动物的指示牌指到新加动物这里即可,新加动物的指示牌再指向下一个动物。工作量是不是会小很多。
由此,我们可出,A动物园便是一种顺序存储结构,它的存储单元是连续的,插入和删除操作都需要挪动大量元素,而链式存储结构则不需要,它的存储空间是任意的,只需要在每个元素中再加入存储下一个元素的指针便可,这样一来,后面的存储位置是不需要变动的。这样,大大减少了算法执行时间,但是也增加了数据存储的大小,因此,这可以说是牺牲了空间换来了时间。
C语言提供了一个良好的工具,便是指针,我们可以通过指针来存储下一个数据存储的位置。每一个数据+指针叫做一个结点,存储的数据叫做数据域,存储指针的地方叫做指针域。
这里需要区分头指针与头结点的区别:
头指针:
头结点:
总得来说,头结点是为了操作方便设立的,不要也可以。
链表相比于顺序存储结构也有其缺点,例如查询操作,它必须从第一个结点开始,指向后一个元素,直到查到为止,时间复杂度为n,相比于顺序存储结构的1,确实麻烦了一些,但是链表的插入与删除将会体现出它优于顺序存储结构的地方。
上面那张图应该清晰地说明了插入操作,就是改变指针指向的位置。删除也是同样的操作,就是将要删除的结点的上一个结点的指针域指向删除结点的下一个结点即可。
下面是代码(这个代码是大话数据结构里的例程代码,我觉得写得已经非常清楚了)
看这段代码建议先复习一下结构体相关知识和结构体指针相关知识,因为我看这段代码看了好久┭┮﹏┭┮
#include "stdio.h" #include "string.h" #include "ctype.h" #include "stdlib.h" #include "math.h" #include "time.h" #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 #define MAXSIZE 20 /* 存储空间初始分配量 */ typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */ typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */ Status visit(ElemType c) { printf("%d ",c); return OK; } typedef struct Node { ElemType data; struct Node *next; }Node; typedef struct Node *LinkList; /* 定义LinkList */ /* 初始化链式线性表 */ Status InitList(LinkList *L) { *L=(LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */ if(!(*L)) /* 存储分配失败 */ return ERROR; (*L)->next=NULL; /* 指针域为空 */ return OK; } /* 初始条件:链式线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE */ Status ListEmpty(LinkList L) { if(L->next) return FALSE; else return TRUE; } /* 初始条件:链式线性表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; } /* 初始条件:链式线性表L已存在。操作结果:返回L中数据元素个数 */ int ListLength(LinkList L) { int i=0; LinkList p=L->next; /* p指向第一个结点 */ while(p) { i++; p=p->next; } return i; } /* 初始条件:链式线性表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为计数器 */ while (p && j<i) /* p不为空或者计数器j还没有等于i时,循环继续 */ { p = p->next; /* 让p指向下一个结点 */ ++j; } if ( !p || j>i ) return ERROR; /* 第i个元素不存在 */ *e = p->data; /* 取第i个元素的数据 */ return OK; } /* 初始条件:链式线性表L已存在 */ /* 操作结果:返回L中第1个与e满足关系的数据元素的位序。 */ /* 若这样的数据元素不存在,则返回值为0 */ int LocateElem(LinkList L,ElemType e) { int i=0; LinkList p=L->next; while(p) { i++; if(p->data==e) /* 找到这样的数据元素 */ return i; p=p->next; } return 0; } /* 初始条件:链式线性表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; while (p && j < i) /* 寻找第i个结点 */ { p = p->next; ++j; } if (!p || j > i) return ERROR; /* 第i个元素不存在 */ s = (LinkList)malloc(sizeof(Node)); /* 生成新结点(C语言标准函数) */ s->data = e; s->next = p->next; /* 将p的后继结点赋值给s的后继 */ p->next = s; /* 将s赋值给p的后继 */ return OK; } /* 初始条件:链式线性表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; while (p->next && j < i) /* 遍历寻找第i个元素 */ { p = p->next; ++j; } if (!(p->next) || j > i) return ERROR; /* 第i个元素不存在 */ q = p->next; p->next = q->next; /* 将q的后继赋值给p的后继 */ *e = q->data; /* 将q结点中的数据给e */ free(q); /* 让系统回收此结点,释放内存 */ return OK; } /* 初始条件:链式线性表L已存在 */ /* 操作结果:依次对L的每个数据元素输出 */ Status ListTraverse(LinkList L) { LinkList p=L->next; while(p) { visit(p->data); p=p->next; } printf("\n"); return OK; } /* 随机产生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; /* 插入到表头 */ } } /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */ void CreateListTail(LinkList *L, int n) { LinkList p,r; int i; srand(time(0)); /* 初始化随机数种子 */ *L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */ r=*L; /* r为指向尾部的结点 */ for (i=0; i<n; i++) { p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */ p->data = rand()%100+1; /* 随机生成100以内的数字 */ r->next=p; /* 将表尾终端结点的指针指向新结点 */ r = p; /* 将当前的新结点定义为表尾终端结点 */ } r->next = NULL; /* 表示当前链表结束 */ } int main() { LinkList L; ElemType e; Status i; int j,k; i=InitList(&L); printf("初始化L后:ListLength(L)=%d\n",ListLength(L)); for(j=1;j<=5;j++) i=ListInsert(&L,1,j); printf("在L的表头依次插入1~5后:L.data="); ListTraverse(L); printf("ListLength(L)=%d \n",ListLength(L)); i=ListEmpty(L); printf("L是否空:i=%d(1:是 0:否)\n",i); i=ClearList(&L); printf("清空L后:ListLength(L)=%d\n",ListLength(L)); i=ListEmpty(L); printf("L是否空:i=%d(1:是 0:否)\n",i); for(j=1;j<=10;j++) ListInsert(&L,j,j); printf("在L的表尾依次插入1~10后:L.data="); ListTraverse(L); printf("ListLength(L)=%d \n",ListLength(L)); ListInsert(&L,1,0); printf("在L的表头插入0后:L.data="); ListTraverse(L); printf("ListLength(L)=%d \n",ListLength(L)); GetElem(L,5,&e); printf("第5个元素的值为:%d\n",e); for(j=3;j<=4;j++) { k=LocateElem(L,j); if(k) printf("第%d个元素的值为%d\n",k,j); else printf("没有值为%d的元素\n",j); } k=ListLength(L); /* k为表长 */ for(j=k+1;j>=k;j--) { i=ListDelete(&L,j,&e); /* 删除第j个数据 */ if(i==ERROR) printf("删除第%d个数据失败\n",j); else printf("删除第%d个的元素值为:%d\n",j,e); } printf("依次输出L的元素:"); ListTraverse(L); j=5; ListDelete(&L,j,&e); /* 删除第5个数据 */ printf("删除第%d个的元素值为:%d\n",j,e); printf("依次输出L的元素:"); ListTraverse(L); i=ClearList(&L); printf("\n清空L后:ListLength(L)=%d\n",ListLength(L)); CreateListHead(&L,20); printf("整体创建L的元素(头插法):"); ListTraverse(L); i=ClearList(&L); printf("\n删除L后:ListLength(L)=%d\n",ListLength(L)); CreateListTail(&L,20); printf("整体创建L的元素(尾插法):"); ListTraverse(L); return 0; }
由于Java并没有指针变量这个概念,也没有结构体,因此,结构体我们将使用类来代替,而指针地址其实就是对象名,我们可以定义一个结点类来存放下一个结点。
那么思路就是:我们定义一个结点类Node用来作为结点,Node类中有数据域和指针域,指针域由Node next来实现,这个指针其实是一个类。定义链表类,这个类中包含头指针和尾指针,还有对链表进行的操作,并且对链表进行泛型化,可以存储任何对象。
下面的代码是我自己写的,可能存在一些不足,封装得也不是很好。至于删除和插入的操作,其实和C语言是一样的,可以看上面。
package date; public class LinkedList<E>{ private Node head;//链表的头结点 private Node rear;//链表的尾指针 private int size=0; private class Node{//结点类 E data;//数据域 Node next;//指针域 public Node(){ this.data=null; this.next=null; } public Node(E e){ this.data=e; this.next=null; } public Node(E e,Node next){ this.data=e; this.next=next; } } public LinkedList() {//尾指针为空,头指针指向尾指针,达到创建链表的作用 this.head=new Node(); this.rear=null; } public void add(int index, E e) { if(index<0 || index>size){ throw new IllegalArgumentException("角标越界"); } Node n = new Node(e); if(size==0){ head.next = n; rear = n; }else if(index ==0){ //头插法 n.next = head.next; head.next = n; }else if(index == size){ //尾插法 rear.next = n; rear = n; }else{ //中间插 Node p = head; for (int i = 0; i < index; i++) { p = p.next; } n.next = p.next; p.next = n; } this.size++; } public E remove(int index) { if(size==0){ throw new IllegalArgumentException("空表"); } if(index<0 || index>=size){ throw new IllegalArgumentException("角标越界"); } E res = null; if(size == 1){//只有一个数据的时候 res = head.next.data; head.next = null; rear = head; }else if(index == 0){ Node del = head.next; res = del.data; head.next = del.next; del.next = null; del = null; }else if(index == size-1){ Node p = head; while(true){ if(p.next != rear){ p = p.next; }else{ break; } } res = p.next.data; p.next = null; rear = p; }else{ Node p = head; for (int i = 0; i < index; i++) { p = p.next; } Node del = p.next; res = del.data; p.next = del.next; del.next = null; del = null; } size--; return res; } public E get(int index) { if(size==0){ throw new IllegalArgumentException("空表"); } if(index<0 || index>size){ throw new IllegalArgumentException("角标越界"); } if(index == 0){ return head.next.data; }else if(index == size-1){ return rear.data; }else{ Node p = head; for (int i = 0; i <= index; i++) { p = p.next; } return p.data; } } public static void main(String[] args) { LinkedList s=new LinkedList(); for(int i=0;i<5;i++) { s.add(i, i); } System.out.println("链表长度:"+s.size); System.out.println("第2个数据:"+s.get(2)); System.out.println("删除第2个数据"); s.remove(2); for(int i=0;i<s.size;i++) { System.out.println("输出"+s.get(i)); } } }
静态链表虽然实现了链表,解决了顺序存储结构的元素移动问题,它只用修改游标即可,但是它由于是使用了数组来进行描述,丢失了链式存储结构的随机存取的特点。因此,静态链表的使用在C语言与Java中使用得并不多,也不推荐使用。
所以这里就不放代码了,大家可以试试,用数组来描述的话会好理解一点。
这两个链表其实没有什么特别之处,循环链表就是将第一结点与尾结点连起来,双向链表就是结点存储的指针域有两个,我们暂且称其为父节点与子结点,父结点就是指向上一个结点的指针,子结点就是指向下一个结点的指针,这样当我们进行搜索的时候,就可以进行双向搜索。
弄懂了单链表,那么无论是循环链表,还是双向链表,还是循环双向链表,其实都是一样的,我们需要弄懂的是链表的基本概念与存储方式。