线性表(亦作顺序表)是最基本、最简单、也是最常用的一种数据结构。线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的。线性表有两种存储结构:
①顺序存储结构,即存储单元在一段连续的地址上存储,常见的数组就是顺序存储结构的线性表;
②链式存储结构,即存储单元在不连续的地址上存储。因为其不连续性,除了要存数据元素信息(数据域)外,还要存储它后继元素(结点)的地址(指针域,链)。学习链式结构最好将结点结构牢记于心,如下图:
链表的每个结点只含有一个指针域就叫做单链表,单链表是其他形式链表以及其他数据结构的基础,所以是这篇文章着重讲的地方。
在这里还要先提前补充一点知识,时间复杂度与大O计法,正是因为有了这些指标,不同情况下不同数据结构有各自的优越之处。时间复杂度,记作:T(n) = O(f(n)),表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,f(n)是问题规模(代码运行次数)的函数。大O阶计算方法:
①用常数1取代时间中所有的加法常数;
②修改后的运行次数函数f(n)中,只保留最高阶,并去除最高阶前面相乘的常数。
举个例子:
在数组中我们存取数据时,都只需要执行一条语句,f(n)=1,则时间复杂度为O(1);
如果需要插入或删除时,最坏情况(一般都是考虑最坏情况)是在第一个元素插入,则需要把所有元素都向后挪动一位,这样需要作n次处理则f(n)=n,时间复杂度为O(n)。
最后再介绍两个函数,就可以开始举实例了。
①void *malloc(unsigned int num_bytes)
动态分配内存指针函数,如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。需要进行强制类型转换。
②void free(void *ptr)
释放ptr指向的存储空间。被释放的空间通常被送入可用存储区池,以后可在调用malloc等函数来再分配。
实例:单链表的各种操作,原本想分类一个函数一个函数讲,太乱了,一起摆又太长了,就当是给那些找Demo的人吧!全都是自己写的,后面的也没注释,真的很糟糕~~
#include "stdio.h" #include "malloc.h" #include "stdlib.h" // 一个结点结构体 typedef struct node { int data; // 数据域,存放数据 struct node *next; // 指针域,存放后继结点的地址 }node; /* * 功能:创建一个单链表 * 输入:链表长度 * 输出:头结点的地址 */ node *createList(int n) { node *h,*p,*s; // 三个结点指针,指向头结点,前驱结点,当前结点 h = (node *)malloc(sizeof(node)); // 头结点分配内存空间,返回指针强制转换为结构体指针 h->data = 0; // 头结点数据域一般不用来存放数据,它的存在意义是为了链表增加删除操作的一致性 p = h; // 前驱结点设为头结点 for(int i = 0; i < n; i++) { s = (node *)malloc(sizeof(node)); // 分配内存给第一个结点,而后循环 printf("输入第%d个元素:",i+1); scanf("%d",&(s->data)); p->next = s; // 将当前结点地址赋给前驱结点的指针域,可以画个图看下 s->next = NULL; // 当前结点为最后一个结点,指针域赋给NULL p = s; // 前驱结点变为当前结点,即当新建一个结点时,当前结点都会变为他的前驱结点 } return h; // 返回头结点指针 } /* * 功能:得到链表长度 * 输入:链表的头结点 * 输出:整形 */ int getSize(node *head) { int size = 0; node *p; p = head; // 前驱结点赋为头结点 while(p->next!=NULL) // 当前驱结点的指针域存放地址,即当前结点,不为NULL时,长度+1 { size++; p = p->next; // 前驱结点要向后移动一位了 } return size; } /* * 功能:得到链表特定位置的数据 * 输入:链表的头结点,获取数据的位置 * 输出:整形 */ int get(node *h,int position) { int result = -1; node *s,*p; p = h; s = p->next; p = s; for(int i = 0 ; i < position; i++) { s = p->next; p = s; } result = s->data; return result; } /* * 功能:插入数据 * 输入:链表的头结点,插入的位置,插入的数据 * 输出:无 */ void insert(node *h,int position,int data) { node *s,*p; s = (node *)malloc(sizeof(node)); s->data = data; s->next = NULL; p = h; for(int i = 0; i < position; i++) { p = p->next; } s->next = p->next; p->next = s; } /* * 功能:动态插入数据在最后一项 * 输入:链表的头结点,插入的数据 * 输出:无 */ void insertAtLast(node *h,int data) { node *s,*p; s = (node *)malloc(sizeof(node)); s->data = data; s->next = NULL; p = h; while(p->next!=NULL) { p = p->next; } p->next = s; } /* * 功能:删除数据 * 输入:链表的头结点,删除数据的位置 * 输出:无 */ void remove(node *h,int position) { node *p,*s; p = h; for(int i = 0; i < position; i++) { p = p->next; } s = p->next; p->next = s->next; free(s); } /* * 功能:寻找查找数据的第一个地址 * 输入:链表的头结点,所查数据 * 输出:结点指针 */ node *search(node *h,int data) { node *p,*s; p = h; while(p->next->data!=data) { p = p->next; } s = p->next; return s; } /* * 功能:在特定的结点后面插入数据 * 输入:链表的头结点,插入的数据 * 输出:无 */ void insertAfterPoint(node *point,int data) { node *s,*temp; s = (node *)malloc(sizeof(node)); s->data = data; temp = point->next; s->next = temp; point->next = s; } /* * 功能:删除整个链表 * 输入:链表的头结点 * 输出:无 */ void deleteList(node *h) { node *p,*s; p = h->next; while(p != NULL) { s = p->next; free(p); p = s; } h->next = NULL; } /* * 功能:打印所有元素的值 * 输入:链表的头结点,链表长度 * 输出:无 */ void printAll(node *h,int n) { for(int i = 0 ; i < n; i++) { printf("%d ",get(h, i)); // 打印链表的每一个值,参数是链表长度 } } void main(int argc, char* argv[]) { node *head; head = createList(4); // 创建一个四个结点的单链表,输入数据1,2,3,4 printAll(head,getSize(head)); // 1 2 3 4 printf("\n"); insert(head,1,101); // 在第二个结点插入101,0对应的是第一个结点 insertAtLast(head,102); // 最后插入102 printAll(head,getSize(head)); // 1 101 2 3 4 102 printf("\n"); remove(head,1); // 移除最后第二个结点 printAll(head,getSize(head)); // 1 2 3 4 102 printf("\n"); node *searchPoint = search(head,2); // 找到数据为2结点的地址 insertAfterPoint(searchPoint,103); // 在其后面插入103 printAll(head,getSize(head)); // 1 2 103 3 4 102 printf("\n"); deleteList(head); // 释放链表 printf("%d",getSize(head)); // 0 printf("\n"); }
打印结果:
如果在我们不知道第i个结点的指针位置,单链表数据结构在插入和删除操作上的时间复杂度也为O(n),但是如果要插入10个数据时,我们找到了i个结点的指针位置,插入第一个数据的算法复杂度是O(n),后面的都是O(1),而数组操作则每次都是O(n)。可见:对于插入或删除数据越频繁的操作,单链表的效率优势就越明显。
总而言之,对于单链表的操作,就是操作头结点,前驱结点,当前结点;前驱结点的指针域数据就是当前结点的指针。
除了单链表外还有循环链表(终端结点的指针域指向头结点)和双向链表(增加一个前驱结点的指针域),以后有需要时再作深入学习。