目录
一、节点的创造 4种方法以及注意事项
1.
struct Node
{
int data;
struct Node* next;
};
struct //声明一个结构体
Node //给结构命名
{ //给结构体node定义内容
int data;
struct Node* next;
};
//该方法仅仅创建了一个名为Node的结构体
创建一个新的节点 struct Node p;
创建一个节点类型指针 struct Node* p;
2.
struct Node
{
int data;
struct Node* next;
}node, *LinkList;
struct //声明一个结构体
Node //给结构命名
{ //给结构体Node定义内容
int data;
struct Node* next;
}
node, //创建一个数据类型为Node的变量,该变量名称为node。
*LinkList; //创建一个Node类型指针, 即相当于 Node* LinkList。
该方法在声明结构体时就已经定义了两个变量,一个Node类型的node节点,一个Node* 类型的节点指针。
创建新的节点 struct Node p;
创建新的节点类型指针 struct Node* p;
申请一个节点空间(通过节点类型指针申请)
struct Node* p=(struct Node*)malloc(sizeof(struct Node))
3.
typedef struct Node
{
int data;
struct Node* next;
}node;
typedef是一个给数据类型重新命名的指令。
例如typedef int a
从此在代码时,int c等同于 a c。
typedef //重新命名
struct Node { //重新命名的对象
int data;
struct Node* next;
}
node; //新名字
创建了一个结构体类型,并且该结构体的名字不再是struct Node,而简化为node.
创建新的节点只需要 node p;//p为节点名。
创建新的节点类型指针 node* p;//p为指向节点的指针名。
通过指针申请一个空间 node* p=(node*)malloc(sizeof(node));
4.
typedef struct Node
{
int data;
struct Node* next;
}LNode,*LinkList;
将struct Node重新命名为LNode,同时给LNode的指针类型起了另外一个名字,LinkList;
也就是说,现在的节点类型名字是LNode,节点指针类型名字有两个,一个是LNode*,另一个是LinkList。
LinkList常用来定义一个头节点指针变量,
定义一个指向头节点的指针就是 LinkList s;
(因为头节点也是LNode节点类型,所以指向它的指针类型应该是LNode*,等价于LinkList)。
LinkList s=(LinkList)malloc(sizeof(LNode))表示(通过指针s)申请了一个节点空间
free(s)表示释放了s所指节点
注:
1.节点都是通过节点指针进行访问和修改以及删除等各种操作的。
2.malloc函数:void* malloc(unsigned int size) 原型说明在stdlib.h头文件中,需要引入
3.free函数:void free(void* p) 原型说明在alloc.h头文件中,需要引用。
void* p表示任意类型的指针变量,所以free(p);//p是节点类型指针
4.由于头结点不储存数据,也可以认为它储存的数据无效,所以可以将一些长度信息放在这里,比如接下来要输入m个值进入m个节点,m可以存在头结点的data中,而不影响链表的使用
二、单链表基本操作
1.单链表分为带头结点和不带头结点的两种类型。
1)带头结点:头指针Head指向头结点,头结点连接后面结点。通过对Head解引用可以访问头结点,否则单独一个头结点是无法被访问到的,他后面所连接的节点就都找不到。这也就是每个节点中都设有一个指向下一个节点的指针的原因。
2)不带头结点:头指针Head直接指向第一个存放数据的节点。同理,通过对Head解引用访问这个链表的第一个节点,以此类推。
struct Node* L;//创建一个头指针,他可能指向头结点或第一个节点
L=(struct Node*)malloc(sizeof(struct Node));//通过头结点开辟一个空间放节点
L->next=NULL;//目前都只有一个节点,该节点是头结点还是第一个节点取决于后面建立链表操作
一)建立链表
2.尾插法建立单链表
//尾插法建立单链表(带头结点)
//该链表节点数据类型是char型
void CreateFromTail(struct Node* L)
{
struct Node* r;//尾结点指针
struct Node* s;
char c;
int flag=1;
r=L;//r和L储存相同的地址,所以指向同一片空间,即他们都指向已经创建的那一个节点。r遍历操作
while(flag)
{
c=getchar();
if(c!='$')
{
s=(struct Node*)malloc(sizeof(struct Node));
s->data=c;//将数据存放到新节点中
r->next=s;//目前尾结点指向新节点
r=s;//移动尾结点指针,使r永远指向尾结点
}
else
{
flag=0;
r->next=NULL;//最终尾结点的指针部分设为空。
}
}
}
//不带头结点
void CreateFromTail(struct Node* L)
{
struct Node* r;
struct Node* s;
char c;
int flag=1;
r=L;
c=getchar();
if(c=='$')
{
flag=0;
}
else
r->data=c;//让头结点存放数据
while(flag)
{
c=getchar();
if(c!='$')
{
s=(struct Node*)malloc(sizeof(struct Node));
s->data=c;
r->next=s;
r=s;
}
else
{
flag=0;
r->next=NULL;
}
}
}
注:
1)单纯定义一个节点类型的指针而不需要通过该指针创建一个新的节点时(比如让它指向某个已经存在的节点),就不需要stuct Node* p=(struct Node*)malloc(sizeof(struct Node)),而直接struct Node* p即可。
2)函数无return,直接对传入的链表操作,类型为void
3)主函数中,操作应为 CreateFromTail(&L);
3.头插法建立单链表
关键在于将新节点插入到当前链表的第一个节点之前。
其他操作不变,只在if-else语句中有改动
if(c!='$')
{
s=(struct Node*)malloc(sizeof(struct Node));
s->data=c;
s->next=L->next;
L->next=s;
}
else
flag=0;
//尾结点指向空,别忘了操作。
二)取元素
//L为带头结点的单链表的头指针
void GetElemenmt(struct Node* L, int i, int &e)
//struct Node* L 指针(地址)传递 原因是传入地址,改变链表;
//int &e 引用传递 是因为需要用e将值带回,通过&e操作
{
struct Node p;
p=L->next;//p指向第一个有元素的节点
int j=1;
while(p && j<i)//需要得到第i-1个位置。通过它存储的地址得到第i个节点的data
{
p=p->next;
j++;
}
if(!p || j>i) return 0;//这里暂时有个疑问,void类型函数中若出现这种情况该怎么处理
e=p->data;
}
存疑01 11
注:指针(地址)传递*和引用传递&
1.指针传递:
- 当进行指针传递的时候,形参(函数定义的时候)是指针变量,实参(函数调用的时候)是一个变量的地址或者是指针变量。调用函数的时候,形参指向实参的地址。
- 指针传递中,函数体内可以通过形参指针改变实参地址空间的内容。
- 若采用指针传递的方式,我们在函数定义和函数声明时使用 *来修饰形参,表示这个变量是指针类型;在进行函数调用时,使用 & 来修饰实参,表示是将该变量的地址作为参数传入函数。
2.引用传递
- 引用实际上是某一个变量的别名,和这个变量具有相同的内存空间。
- 实参把变量传递给形参引用,相当于形参是实参变量的别名,对形参的修改都是直接修改实参。
- 在类的成员函数中经常用到类的引用对象作为形参,大大的提高代码的效率
- 引用传递可以看成在值传递的基础上,在函数定义和声明的形参变量前加一个 &,其它的使用和值传递完全相同,因此也看出引用传递更加方便(在函数调用时,直接给变量就行,和值传递一样,不需要任何修饰符)。
//带头结点,头指针是L,取第i个位置的data
int j=0;
struct Node* p;
p=L;
for(j=0;j<=i;j++)
{
p=p->next;
}//现在p指向第i个节点位置
int datanum=p->data;
三)插入节点
不写函数了,直接在主函数内部实现
//带头结点,L为头指针,在第i个位置插入新元素e
int j=0;
struct Node* p;
p=L;
for(j=0;j<i;j++)
{
p=p->next;
}//循环结束后,p指向第i-1个节点
struct Node* s=(struct Node*)malloc(sizeof(struct Node));
s->data=e;
s->next=p->next;
p->next=s;//关键三行
四)删除节点
同上
//带头结点,L为头指针,删除第i个位置的节点,并用e存储删除节点的data
int j;
struct Node* p;
p=L;
for(j=0;j<i;j++)
{
p=p->next;
}//此时p指向第i-1个节点
struct Node* q;
q=p;
q=q->next;//不要写成q++ 指针运算
//可以合并写成q=p->next; 这里只是想提醒上面小点
p->next=q->next;
e=q->data;
free(q);
注:q++是指针运算,它所指向的变量类型占据几个字节,q就增加几;要想让q指向下一个节点,需要q=q->next; 存疑02
三、循环链表
基本操作同线性链表,只是‘最后一个节点’指向头结点,即p->next=L;p指向‘最后一个节点’,L是头结点的地址。
若想在循环链表中找到单链表的表尾,可以判断p->next!=L是否成立。
四、双向链表
每个节点的指针域中包含两个指针,一个指向前驱,另一个指向后继。
typedef struct DulNode
{
int data;
struct DulNode *prior;
struct DulNode * next;
}DulNode, *DLinkList;
双链表一般也是由头指针head唯一确定
查询和单链表相同,插入和删除需要同时修改两个方向上的指针。而且特别注意顺序。
一)结点删除
设p指向双向链表中某个节点,删除*p (*p表示该节点)
p->prior->next=p->next;
p->next->prior=p->prior;//修改存放的地址同时断开了两个箭头
free(q);//释放空间,剩余两个箭头也自动消失
二)结点插入
注意顺序!!!
//设p指向双向链表中某节点,s指向待插入的值为x的新节点,将*s插入到*p前面
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
五、双向循环链表
在双链表的基础上,最后一个节点的next指针指向头结点,头结点的prior指针指向最后一个节点。
其特点是p->prior->next=p=p->next->prior(不循环时,除了头尾节点以外也是如此)
删除的节点是无头结点的第一个节点时,记得修改头指针(不太确定,再想想)