单链表:节点只有一个指针域的链表,称为单链表或线性链表。
特点:
①结点位置任意,逻辑与物理无关
②访问时只能通过头指针进入链表,并通过每一个结点的指针域依次向后扫描其余节点,所以寻找第一个结点和最后一个时间不等、
一、容易混淆的概念:
头指针:是指向链表中第一个结点的指针(可带可不带)
首元结点:是指链表(线性表)中存储的第一个数据元素a1的结点
头结点:是在链表的首元结点之前附设的一个结点
空表:无头结点时,头指针为空时表示空表;有头结点时,当头结点的指针域为空表示空表
头结点的好处:首元结点地址在头结点指针域中,故处理链表的第一位置和其它位置一致,无需特殊处理;便于统一处理空表和非空表
头结点的数据域:头结点数据域可以为空,也可以存放表长等附加信息,但是此结点不可以计入链表长度值。
二、定义与表示:
单链表是由头指针唯一确定,故单链表可以用头指针名字命名,若头指针名为L,则把链表称表L
typedef struct _Node{
int data;
struct _Node *next;
}node, *linklist;
//结点类型为—Node
//指向结构体—Node的指针类型叫linklist
下一次定义结点直接 _Node L;
定义指向结点指针可以_Node *p;也可以linklist p;
例如:存储学生学号姓名成绩的单链表结点类型如下
typedef struct student {
char num[8]; //数据域
char name[8]; //数据域
int score; //数据域
struct student *next; //指针域
} Lnode, *LinkList;
为了统一操作,我们通常这样来定义:
typedef struct {
char num[8];
char name[8];
int score;
} ElemType;
三、基本操作:
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
#define OK 1;
#define ERROR -1;
算法1:单链表的初始化(构造一个空表)
步骤:生成新节点作为头结点,用头指针L指向头结点;将头结点的指针域置空;
Status InitList_L(LinkList &L) {//若是c语言则为linklist *L;
L = new LNode;
//或者 L = (LinkList)malloc(sizeof(LNode)); LNode 是上面定义出来的结点类型
L -> next = NULL;
//指针指向的结构的成员变量用->
return OK;
}
算法2:判断链表是否为空:链表为空,相当于头结点的指针域为空
bool ListEmpty(LinkList L) {
if (L -> next) return 0;//非空返回0
else return 1;
}
算法3:单链表的销毁,链表销毁后就不存在了.思路是从头指针开始,依次释放所有结点
p用来删除先前new出来的结点空间,L则是不断向前移动,直到移动到最后空
Status DestroyList_L(LinkList &L) {
Lnode *p;//或linklist p;
while (L) {
p = L;
L = L->next; //这一步很重要!!!最后指向空时,p指向最后一个结点,刚好释放所有空间
delete p;//若是c语言则为free(p);
}
return OK;
}
算法4:清空链表
链表仍然存在,但是链表中没有元素,成为空链表了(头指针,头结点仍然存在)
第一步:
如果没有头结点:p = L; 此时p指向首元结点
如果有头结点:p = L->next;此时p通过L指向的头结点的指针域指向首元结点
第二步:
用指针变量q记录下一个结点地址,一边p删除后找不到下一节点
反复执行:p = q; q = q->next; 注意这两部不可以交换,以面地址丢失
结束条件:p == NULL; 循环条件:p != NULL;
Status ClearList(LinkList &L) { //将L重置为空表
LinkList *p, *q;
p = L->next; //将p指向首元结点
while (p) { //p没有到表尾,p非空意味着还有结点还可以删,p空了就说明结点删完了
q = p->next; //q用来记录下一个结点地址,以面下一节点地址丢失
delete p; //安心删除p所指的空间
p = q; //p跟上q
}
L->next = NULL; //将头结点指向空,此时为空表
return OK; //返回成功
}
算法5:求单链表表长
从首元结点开始依次计数所有结点
int ListLength_L(LinkList L) { //返回L中数据元素个数
LinkList p = L->next; //p指向首元结点
int i = 0;
while (p) { //p非空结点就计数
i++;
p = p->next;
}
return i;
}
算法6:取值——取单链表当中第i个元素
从链表头指针出发,顺着链表出发,知道搜索到第i个节点位置,链表不是随机存取结构
Status GetElem_L(LinkList L, int i, ElemType &e) { //获取线性表L中的某个数据元素的内容,通过变量e返回
p = L->next; j = 1; //初始化,p指向首元结点
while (p && j<i) { //向后扫描,直到p指向第i个元素p为空
p = p->next; ++j;
}
if (!p || j>i) return ERROR; //第i个元素不存在
e = p->data; //取第i个元素
return OK;
}
+补充:一种比较投机取巧的写法(也正确)
int get_node(linklist L, int i){
linklist p;
p = L->next;
int cnt = 1;
while(p){
if (cnt != i){
p = p->next;
cnt ++;
}
else return p->data;
}
return ERROR;
}
算法7:按值查找(因线性链表只能顺序存取,即在查找时要从头指针找起,时间复杂度为O(n).)
查找 1:返回地址
linklist check_node1(linklist L, int key){
linklist p;
p = L->next;
int cnt = 1;
while(p->data != key && p){
p = p->next;
cnt ++;
}
if(!p)return 0;//
else return p;//这两行为了统一化书写也可以直接写成 return p;
}
查找 2:返回是第几个元素
int check_node2(linklist L, int key){
linklist p;
p = L->next;
int cnt = 1;
while(p->data != key && p){
p = p->next;
cnt ++;
}
if(!p)return 0;
else return cnt;
}
算法8:插入(在第i个元素前插入数据元素e)
1、首先找到a_{i-1}的存储位置p
2、生成一个数据域为e的新节点s,插入新节点
3、新节点指针域指向ai,结点a_{i-1}的指针域指向新节点
//在L第i个元素之前插入数据元素 e,只能在第i个元素之前,而不能在之后,这意味着,要在链表尾部加元素是不能通过该方法解决的。
Status ListInsert_L(LinkList &L, int i, ElemType e) {
//用引用的方式传入参数,可以修改实参
linklist p = L; int j = 0; //从位置为0就可以插入了,也就是头结点,而非首元结点
while (p && j<i-1) { //寻找第i-1个结点,p指向i-1结点
p = p->next;
++j;
}
if (!p || j>i-1) return ERROR; //i大于表长+1或者小于1,插入位置非法
s = new Lnode; s->data = e; //生成新节点s,将节点s的数据域置为e
s->next = p->next;
p->next = s; //这两步的顺序不能颠倒
return OK;
}
算法9:删除(第i个元素)
1、首先找到a_{i-1}的存储位置p,保存要删除ai的值
2、令p->指向a_{i+1},p->next=p->next->next //指向后面的后面
3、释放结点ai的空间
Status ListDelete_L(LinkList &L, int i, ElemType &e) {
p = L; j = 0; //最开始指向头结点,也就是首元结点之前
while (p->next && j<i-1) {
p = p->next; ++j;
} //寻找第i个结点就是从首元结点开始向后走i-1次,并令p指向要找结点的前驱
if (!(p->next) || j > i-1) return ERROR;
//删除位置不合法p指向结点的指针域是空表示指向表尾,j>i-1表示所找的前驱位置小于零;这两种情况一种是所找i结点小于等于就0,一种是找的i结点位置大于表尾元素位置,都是越界查找了,因此返回ERROR
q = p->next; //找到目标节点的前驱保存目标节点
p->next = q->next; //让前驱结点直接指向后继节点
e = q->data; //保存删除节点的数据域,也可以不用保存,万一删除结点是有用的,看情况操作
delete q; //释放删除节点的空间
return OK; //返回工作状态成功
}
算法10:头插法建立单链表(时间复杂度是O(n))
1、从一个空表开始,重复读入数据;
2、生成新节点,将读入数据存放在新节点数据域中
3、从最后一个结点开始,依次将各个结点插入到链表前端
void CreateList_H(LinkList &L, int n) {//n为新增的结点个数
L=new Lnode;
L->next=NULL; //创建头结点,并将指针域置空
for (i=n; i>0; --i) {
LinkList p= new LNode, //生成新结点p
cin>>p=>data; //入元素值 scanf(&p->data);
p->next=L>next; //把原来头结点之后的所有数据全部插到新结点p的后面
L->next=p;
}
}
算法11:尾插法建立单链表(时间复杂度为O(n))
1.从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
2.初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新节点插入尾结点后,r指向新节点。
//正位序输入n个元素的值,建立带头结点的单链表L
void CreateList_R(LinkList &L, int n){
L=new LNode; L->next=NULL;
LinkList r, p;
r = L;//尾指针r指向头结点
for(i=0; i<n; i++) {
p= new LNode; cin>>p->data;//生成新结点,输入元素值
p->next=NULL;
r->next=p; //新结点插入到表尾
r=p;//r指向新的尾结点
}
}
注: 在插入和删除操作中,
因线性链表不需要移动元素,只要修改指针,一般时间复杂度为O(1)
//(特指只移动指针域,不包含查找前驱结点的过程)
但是,若要在单链表中进行前插或删除操作,由于要从头查找前驱结点,时间复杂度为O(n)