1.单链表结构
我们想表示一个数据与直接后继数据的逻辑关系,也就是存储数据和存储该数据的下一个数据的地址。我们把这样的存储结构称作结点。
data | next |
data:存储数据,叫数据域
next:存储后一数据地址,叫指针域
head:指向第一个结点,叫头指针
第一个结点之前的一个结点叫做头结点,它可以不存储任何东西,也可以存储单链表不包括的元素值。
图片来源于博客:先鱼鲨生
存储结构定义
Q1:为什么不能用Node *next?
编译器无法识别,因为计算机还没有执行完typedef这块代码,所以必须用struct Node完整来表示。
Q2:为什么不能用顺序表ElemType *next?
那是因为ElemType使用对象是数据域,Node才是指针域使用的,如果使用前者,它指向的数据本身,而非节点使用的。
typedef struct Node
{
ElemType data; //数据域
struct Node *next; //在Node结构体中定义一个指针成员next
}Node,*LinkList; //Node是给struct Node取的别名,*LinkList是给struct Node *结构体的指针取的别名
2.单链表基本操作
2.1 建立单链表
LinkList *Create(LinkList *L) {
Node *r, *s; //定义*r为尾结点指针, *s为新结点指针
ElemType c; //是数据变量
int i; //i是循环变量
//建立头节点, 初始尾结点指向头结点
*L = (LinkList*) malloc(sizeof(Node)); //给头结点分配内存空间
r = *L; //r初始指向头结点
for (i=1; i<=3; i++) { //循环3次添加3个新节点
printf("请输入第%d个数:", i);
scanf("%d", &c); //从控制台接收数值给c
s = (LinkList*)malloc(sizeof(Node)); //给新结点分配内存空间
s ->data = c; //新结点的数据域赋值c
s->next = NULL; //新结点的指针域为空
//下面是给链表尾结点挂接新结点
r->next = s; //将新结点插入当前表尾
r = s; //r指向当前表尾
}
return L; //返回头节点
}
不专业的帮助理解:首先头指针和尾指针指向头结点,*L为头指针,r为尾指针。
其次,生成一个新节点,s为新结点指针,为s弄一个存储空间,c为数据,把数据存储到数据域data,此时指针域next仍未空,因为咱们还没把这个新结点s插到表尾,以至于还没生成这个的下一个新结点,这个s-next无法储存地址。
最后把这个结点弄到表尾,也就是用尾指针r-next地址存储这个新节点s,然后这个s结点成为尾结点,尾指针r就要从原来指向*L结点到现在尾s结点。
温馨提示:插入元素数据的位置i的范围:1≤i≤n+1
2.2插入数据
采用的原则:“先连后断”--重感情的,哈哈哈。
Status ListInsert(LinkList *L, int i, ElemType e) {
Node *p, *s; //*p为尾结点, *s为新节点
int j=0; //记录当前的位置
// 步骤1: 查找插入位置的前驱节点
p = *L; //尾结点p指向头节点, 下面通过while去找到 要插入位置的尾结点
while( p!=NULL && j<i-1) {
p = p->next; //p节点往下一节点移动
j++;// j要记录移动位置
}
if (j!=i-1) return FALSE; //如果没有找到 前驱节点位置,就直接返回失败状态
// 步骤 2:生成新节点
s = (LinkList*)malloc(sizeof(Node)) ; //分配内存空间
s ->data = e; //把数据元素赋值给新节点的数据域上
// 步骤3: 重新关联指针域
s ->next = p->next; //将前驱节点的下一个节点连到 新节点的下一个节点上
p->next =s; //把新节点指向前驱节点的后继指针域
return TRUE;//返回完成的状态
}
Q3:插入带来哪些结构性改变?(先不管那些节点个数变化)
首先我们改变是持续的,所以要用LinkList *L;其次,我们会引起元素变化e;最后选择那个位置进行插入。
Q4:为什么是i-1?
由于头结点位置是0,但遍历又不得不从它开始,所以,i-1是我们需要找到插入的前结点位置。类比于数组的索引。
不专业的帮助理解:我们确定要插入那个位置,引入i,其次我们要记录找了几次,引入j,找到后带来尾结点和新节点指向问题变化,引入*p和*s。首先,p初值指向头结点。然后循环寻找,循环就一定有条件,首先要告诉它循环到表尾停止,其次循坏到我想出入前一个位置就行。
Q5:既然我知道它要弄到我要的前一个位置不就好,为啥还要有前一个条件?
那是因为操作者并不知道生成了多少个结点,如果只有3个结点(含头结点),她告诉系统我要插第4个,但根本不存在。所以最终我们以2不等于3告诉它插入失败。但实际搜索到表尾都没找到,说明插入位置大于实际的结点个数,根本不用多此一举进行循环。
承接上面话继续,我们需要尾结点p不断移动移动去找到下一个,直到找到i-1为止。如果j=i-1说明找对了,便开始给这个新的结点开辟存储空间,把元素存储到数据域,由于AB2个结点是个讲感情的人,A想尽管新朋友C加入也不能忘老朋友B,所以加入结点C就是桥梁,A先看到新节点C和原来后结点B成为好朋友才愿意接纳C,所以s->next=p->next;然后p->next才愿意接受s。因此告诉大家这两行代码不能换位置(先连后断)。
2.3删除数据
首先,我们进行链表删除,就要想到释放空间,运用到free()函数,这个要条件反射哦!
原则:先指向,在搭桥,后拆桥
Status ListDelete(LinkList *L, int i, ElemType *e) {
Node *p, *r; //*p是i-1的节点, *r是被删除的节点
int j=0;
p=*L;//p初值指向头结点
while (p && j<i-1) {
p = p->next; //p节点往下一个节点移动
j++; //记录移动的位置
}
if (j != i-1) return FALSE; //没有找打i-1的位置
r = p->next; //r指向要删除的节点
p->next = p->next->next; //p的指针域指向删除节点的下一个节点上
e= r->data; //要删除的数据元素赋值给e
free(r); //把删除的节点r释放内存空间
return TRUE;
}
Q6:为什么不能是ElemType e?
我们删除也需要修改外部,ElemType e会创建副本,其实质并未改变。
Q7:那与顺序表插入ElemType e又有怎样区别?
顺序表是通过数据副本(值传递),它只用读取数据,不用返回,好比打印机,它通过复印就可以。但这里需要外部知道删除内容,不仅仅只让它消失,就好比一个亲人去世,尽管肉体消失(实质),可我们怀念他可以通过照片(外部留存)。
不专业帮助理解:同添加一样,要找到前一个结点。从头结点开始,循环条件也不变。r先指p的后一节点(相当于保存),然后搭桥p的后继点的连接删掉的后一结点的后继点。然后把删除的元素给e(交给它寄存),释放r。
2.4查询数据
不做讲解,sorry!
完整代码见资源!!!