目录
今天要记录的是线性表的单链表,上节内容提到过顺序表存取是随机的非常快捷;但是不方便插入或者改变容量,所以这里就提出了单链表的概念
一、什么是单链表,如何定义单链表
单链表简单地说就是将一个数据元素分为数据域和指针域,指针域用于存放当前数据元素的后继地址,方便找到后面的元素
在介绍单链表的操作之前,我们要先了解两个知识点
- typedef关键字
这是用于对数据类型进行重命名的关键字,可以使后面使用这个数据类型时更加方便,它的使用语法如下:
学会这个语法之后我们就可以定义一个单链表:
typedef struct LNode
{
int data;//存放一个数据元素
LNode* next;//指向下一个节点的指针
}LNode,*Linklist;//将struct LNode重命名为LNode和*Linklist
其实这块代码和下面这块代码是一样的
struct LNode
{
int data;//存放一个数据元素
LNode* next;//指向下一个节点的指针
};
typedef struct LNode LNode;//将struct LNode重命名为LNode
typedef struct LNode* Linklist;//将struct LNode重命
从这里就可以看出typedef关键字的方便之处了;另外也许有人会疑惑为什么要为同一个结构类型重命名两个名字,其实这里是为了提高代码可读性,Linklist代表创建一个链表,而LNode指一个节点
- 单链表分为有头节点和无头节点;总的来说有头节点的单链表操作会更加的简单,下面分别对有头节点和没有头节点的单链表进行初始化
没有头节点
bool init_list(Linklist& l)
{
l = NULL;//没有任何节点
return true;
}
有头节点
bool init_list(Linklist&l)
{
l = new LNode;//分配一个头节点
if (l == NULL)return false;//内存不足,分配失败
l->next = NULL;//分配成功,头节点置空
return true;
}
二、单链表的插入和删除
单链表的插入可以分为按位序插入和按值插入,我们这里就只将带头结点和不带头节点的按位插入
1.按位插入
bool insert_list(Linklist& l,int i,int e)
{
if (i < 1)return false;//检查i是否符合逻辑
LNode* p=l;//用p来指向头节点遍历到i-1
int j=0;//计数之用,方便循环
while (j < i - 1 &&p!=NULL)
{
p = p->next;//不断遍历
j++;
}
//做一个超出范围的检查
if (p == NULL) { cout << "超出插入范围" << endl; return false; }
LNode* s=new LNode;//s作为新节点插入
s->data = e;
s->next = p->next;
p->next = s;
return true;//插入成功
}
不带头节点就会在处理第一个节点时略微麻烦一点,接下来就看代码实操:
bool insert(Linklist& l, int i, int e) {
// 如果索引无效(小于1),则返回false
if (i < 1) return false;
// 如果在链表头部插入(i == 1)
if (i == 1) {
LNode* s = new LNode(e); // 使用构造函数初始化数据
s->next = l;
l = s;
return true;
}
// 找到插入点的前一个节点
int j = 1;
LNode* p = l;
while (j < i - 1 && p != nullptr) {
p = p->next;
j++;
}
// 如果索引超出链表长度,则返回false
if (p == nullptr) return false;
// 在找到的位置插入新节点
LNode* s = new LNode(e);
s->next = p->next;
p->next = s;
return true;
}
这没有头指针的麻烦处理就在于指向首元节点的是p指针还是l,有头节点就可以将其一致处理,用p指针就可以直接进行衔接
2.指定节点的前插操作
由于链表的特性,指定节点都只能找到它后面的元素,因为指针域存放的都是后面的地址,如果要想找到其前面的元素就只有找到链表名字(头指针)通过遍历来找到它的前驱节点,但这样它的时间复杂度就是O(n)了
所以这里引进一种新思路,既然我们无法直接进行前驱节点的寻找,那么我们就传入要插入的数据和当前节点,在当前节点p后面插入一个节点并将p中的数据复制过来然后将要传入的数据放在p中,这样就实现了一个前插操作,并且时间复杂度还是O(1)下面是代码演示:
bool infrontinsert(LNode* p,int e)
{
if (p == NULL)return false;
LNode* s = new LNode{p->data,p->next};//分配内存并将p节点复制过来
if (s == NULL)return false;//分配内存失败
p->data = e;//p节点存放新数据
p->next = s;//更新p节点的指向
return true;
}
3.按位删除带头结点的节点
同样我们删除某个节点时也有两种想法,最普通的就是我们用遍历来找到要删除的前驱节点,改变它的指针域使它跳过当前节点,最后释放当前节点的内存就完成了删除操作
1.遍历删除节点
下面是代码演示:
bool delete_lnode(Linklist&l,int i)
{
//判断范围不合法以及空表的情况
if (i < 1||l->next==NULL)return false;
int j = 0;//用于计数遍历
LNode* p=l;//当前指针遍历链表
while (j < i - 1 && p ->next!= NULL)
{
p = p->next;
j++;
}
if (p->next == NULL)return false;//i超出范围
LNode* q = p->next;
p->next = p->next->next;
delete q;
return true;
}
2.替身法
这和前面的前插法有点类似,由于单链表的局限性(无法逆向检索),我们可以用替身的办法进行删除节点,因为我们无法改变目标节点的前驱节点的指针域,所以目标节点是无法释放的(如果释放链表就会断开),那么我们就把后继节点复制到当前目标的数据域(覆盖),这样我们可以将后继节点进行删除,这样也不会出现地址丢失的情况,缺点是如果我们要删除最后一个节点需要单独处理,因为最后一个节点没有后继节点进行替身了,下面是代码实现:
bool delete_lnode(LNode*& p)
{//如果p是最后一个节点就直接删除
if (p->next == NULL) { delete p; p = NULL; return true; }
//p后面还有节点,进行替换删除操作
LNode* s = p->next;
p->data = s->data;
p->next = s->next;
delete s;
return true;
}
这里我对删除最后一个节点的情况进行了判断处理;另外这里涉及一个问题:在我尝试不适用引用时这个指针是无法被修改指向的,因为对于这个指针本身仍然是值传递,所以我们如果想要删除这个指针我们就要使用引用或者二级指针
三、单链表的查找
1.按位查找
按位查找其实在前面插入部分已经实现过了,这里我们只需要修改i为当前要提取的元素就可以了,但是需要注意的是如果查找失败我们需要返回一个空,而且在溢出时我们也会返回一个空
//按位查找(带头结点)
LNode* getelem(Linklist l, int i)
{
if (i < 1||l->next==NULL)return NULL;
int j = 0;
LNode* p = l;
while (j < i && p != NULL)
{
p = p->next;
j++;
}
return p;
}
2.按值查找
LNode* getelem2(Linklist l, int e)
{
// 初始化指针p,使其指向链表的第一个实际数据节点(即头节点的下一个节点)
LNode* p = l->next;
// 循环遍历链表,直到找到数据域等于e的节点或遍历完整个链表
while (p != NULL && p->data != e)
{
p = p->next;
}
// 返回找到的节点指针,如果未找到则返回NULL
return p;
}
3.统计表长
int length(Linklist l)
{
int i = 0;
LNode* p = l;
while (p->next != NULL)
{
p = p->next;
i++;
}
return i;
}
四、单链表的建立
建立一个单链表通常有两种方法:头插法和尾插法,这里我们就依次介绍
1.尾插法
其实用尾插法我们可以调用后插法的函数,不过这样我们就要每次都遍历一次,最后时间复杂度达到了O(n^2),因此这里我们需要定义一个尾指针,这样每次插入一个元素尾指针就指向这个元素,不用每次都遍历,可以降低时间复杂度
void tailinsert(Linklist& l)
{
init_list(l);//初始化一个有头节点的链表
int x;
LNode* p=NULL,*r=l;//p用于创建新节点,r作为哨兵节点
cin >> x;
while (x != 0)
{
p = new LNode{ x,NULL };//创建新节点并初始化
r->next = p;//连接新节点
r = r->next;//这里等价r=s;用于指向新的表尾节点
cin >> x;
}
}
2.头插法
void headinsert(Linklist&l)
{
init_list(l);
LNode* p = NULL;
int x=0;
cin >> x;
while (x != 0)
{
p = new LNode{x,l->next};
l->next = p;
cin >> x;
}
}
五、单链表的销毁和清空
1.销毁
void destroylist(Linklist& l)
{
LNode* p = l;
while (l)
{
l = l->next;
delete p;
p = l;
}
}
2.清空
void empty(Linklist& l)
{
LNode* p = l->next, * q = l->next;
while (p)
{
q = q->next;
delete p;
p = q;
}
l->next = NULL;
}
六、各种功能源码
typedef struct LNode
{
int data;//存放一个数据元素
LNode* next;//指向下一个节点的指针
}LNode,*Linklist;//将struct LNode重命名为LNode和*Linklist
bool init_list(Linklist& l)
{
l = new LNode;
if (l == NULL)return false;
l->next = NULL;
return true;
}
bool insert_list(Linklist& l,int i,int e)
{
if (i < 1)return false;//检查i是否符合逻辑
LNode* p=l;//用p来指向头节点遍历到i-1
int j=0;//计数之用,方便循环
while (j < i - 1 &&p!=NULL)
{
p = p->next;//不断遍历
j++;
}
//做一个超出范围的检查
if (p == NULL) { cout << "超出插入范围" << endl; return false; }
LNode* s=new LNode;//s作为新节点插入
s->data = e;
s->next = p->next;
p->next = s;
return true;//插入成功
}
bool insert(Linklist& l, int i, int e)
{
if (i < 1)return false;
if (i == 1)
{
LNode* s = new LNode;
s->data = e;
s->next = l;
l = s;
return true;
}
int j=1;
LNode* p = l;
while (j < i - 1 && p != NULL)
{
p = p->next;
j++;
}
if (p == NULL)return false;
LNode* s = new LNode;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
bool infrontinsert(LNode* p,int e)
{
if (p == NULL)return false;
LNode* s = new LNode{p->data,p->next};
if (s == NULL)return false;
p->data = e;
p->next = s;
return true;
}
bool delete_lnode(Linklist&l,int i)
{
//判断范围不合法以及空表的情况
if (i < 1||l->next==NULL)return false;
int j = 0;//用于计数遍历
LNode* p=l;//当前指针遍历链表
while (j < i - 1 && p ->next!= NULL)
{
p = p->next;
j++;
}
if (p->next == NULL)return false;//i超出范围
LNode* q = p->next;
p->next = p->next->next;
delete q;
return true;
}
bool delete_lnode(LNode*& p)
{//如果p是最后一个节点就直接删除
if (p->next == NULL) { delete p; p = NULL; return true; }
//p后面还有节点,进行替换删除操作
LNode* s = p->next;
p->data = s->data;
p->next = s->next;
delete s;
return true;
}
//按位查找(带头结点)
LNode* getelem(Linklist l, int i)
{
if (i < 1||l->next==NULL)return NULL;
int j = 0;
LNode* p = l;
while (j < i && p != NULL)
{
p = p->next;
j++;
}
return p;
}
LNode* getelem2(Linklist l, int e)
{
LNode* p=l->next;
while (p->data != e&&p!=NULL)
{
p = p->next;
}
return p;
}
int length(Linklist l)
{
int i = 0;
LNode* p = l;
while (p->next != NULL)
{
p = p->next;
i++;
}
return i;
}
void tailinsert(Linklist& l)
{
init_list(l);//初始化一个有头节点的链表
int x;
LNode* p=NULL,*r=l;//p用于创建新节点,r作为哨兵节点
cin >> x;
while (x != 0)
{
p = new LNode{ x,NULL };//创建新节点并初始化
r->next = p;//连接新节点
r = r->next;//这里等价r=s;用于指向新的表尾节点
cin >> x;
}
}
void headinsert(Linklist&l)
{
init_list(l);
LNode* p = NULL;
int x=0;
cin >> x;
while (x != 0)
{
p = new LNode{x,l->next};
l->next = p;
cin >> x;
}
}
void print(Linklist l)
{
LNode* p=l;
while (p)
{
p = p->next;
cout << p->data << "\t";
}
cout << endl;
}
void destroylist(Linklist& l)
{
LNode* p = l;
while (l)
{
l = l->next;
delete p;
p = l;
}
}
void empty(Linklist& l)
{
LNode* p = l->next, * q = l->next;
while (p)
{
q = q->next;
delete p;
p = q;
}
l->next = NULL;
}
int main()
{
Linklist l;
headinsert(l);
print(l);
}