单链表讲解

一.链表的概念以及结构

链表是一种物理结构上不连续,逻辑结构上连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 

链表的结构与火车是类似的,一节一节的,数据就像乘客一样在车厢中一样。

与顺序表不同的是,链表里的每节"车厢"都是独立申请的空间,我们将这样的空间称为节点或者结点

 既然链表是一节一节的结点构成的,那么这样的结点是怎么连接的呢?

链表中的节点一般是通过结构体来实现的,结构体中的存储着数据和下一个节点的地址,这样我们就可以访问下一个节点中的数据了。

节点:

typedef struct SListNode
{
	SLTDataType val;
	struct SListNode* next;
}SLTNode;

为了使用方便我们可以对其重命名。

二.单链表的实现

typedef int SLTDataType;
typedef struct SListNode
{
 SLTDataType data; //节点数据
 struct SListNode* next; //指针保存下⼀个节点的地址
}SLTNode;
void SLTPrint(SLTNode* phead);
//头部插⼊删除/尾部插⼊删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

首先我们来思考如何打印数据呢?

void SLTPrint(SLTNode* phead);

这个参数是指向第一个头节点的指针,为什么要使用指针呢?

通过指针我们可以访问这个结构体中的元素,如果我们不使用指针,形参是实参的一份临时拷贝,如果数据过大,这样会很浪费空间(我们这里存储的数据是整形,为了泛用性,我们还是要使用一级指针),直接使用会降低程序性能。

phead->data就可以访问到数据了,那么下一个节点的数据呢?

这时候就会用到我们在结构体中存储的指针了,这个指针指向下一个结构体,所以再通过下一个结构体我们就可以访问下一个结构体的数据,同理,下下个结构体的指针同理。

void SLTPrint(SLTNode* phead) {
	SLTNode* pcur = phead;
	while (pcur) {
		printf("%d->>",pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
};

通常习惯上,我们是不会改变直接传过来的参数的,所以我们要使用的话,就再使用一个变量来进行接收。

这里pcur不为NULL时,代表我们的指针并没有走到结构体的末尾,我们就可以以它作为限制条件,每经过一个节点,就打印一次数据,直到遇到空指针停止。 

然后就是要完成指定位置之前插入数据了。

假设有一个链表1->>2->>3->>5->>NULL.

我们要在数据5的前面插入4.可是在插入之前,我们要找到数据五的位置才能插入。

如何查找元素呢?

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

第一个参数是指向第一个节点的指针,第二个是要寻找的数据,虽然这里可能会存在相同的数字,但是这只是我们练习的场景,假设我们要存储人的身份信息,这是不可能存在相同的人的,所以我们这里假设数据不相同。

最后返回这个节点的地址。

SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
	assert(phead);
	SLTNode* pcur = phead;
	while (pcur) {
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
};

和打印数据相同。我们只需要遍历整个数组即可,如果找到就返回这个节点的地址,否则就返回空。

接着我们再来完成指定位置插入数据。

即使我们找打这个节点的地址,我们要在它之前插入数据,也就是再创建一个节点,然后把这个节点的next指针指向找的节点,这样我们就在指定节点前面插入了数据。

但是我们新建的这个节点是没有被节点指向的,所以我们还要让前一个节点指向新的节点。

这个SLTBuyNode是创建新节点函数,我们在插入数据时,会频繁用到这个代码,如果重复的写,就太浪费时间了,所以我们单独封装一个函数来完成这个功能。

SLTNode* SLTBuyNode(SLTDataType x) {
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("malloc");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}
		SLTNode* pcur = *pphead;
		while (pcur->next != pos) {
			pcur = pcur->next;
		}
		pcur->next = SLTBuyNode(x);
		pcur->next->next = pos;

可是这样写就完了吗?

如果我们要在链表的第一个节点之前插入数据,我们会发现这个代码循环是根本没有进入的,并且会访问NULL地址,这是非法的。

所以为了防止这样的问题,我们还需要额外区分头插的情况。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
	assert(pphead);
	assert(*pphead);
	if (*pphead == pos) {
		*pphead = SLTBuyNode(x);
		(*pphead)->next = pos;
	}
	else {
		SLTNode* pcur = *pphead;
		while (pcur->next != pos) {
			pcur = pcur->next;
		}
		pcur->next = SLTBuyNode(x);
		pcur->next->next = pos;
	}
};

 然后我们来思考,为什么这里要使用二级指针,我们使用指针的目的是为了在函数中改变指针所指向的变量的值,这里就是传值和传址的区别了,我们在头插的过程中指向头节点的指针,是会改变的,如果我们传一级指针,是改变不了这个指针的,所以我们要使用二级指针。

然后就是删除数据函数

void SLTErase(SLTNode** pphead, SLTNode* pos);

这里与插入函数一样,是需要考虑头删的。

void SLTErase(SLTNode** pphead, SLTNode* pos) {
	assert(pphead);
	assert(*pphead);
	SLTNode* pcur = *pphead;
	if (pos == *pphead) {
		*pphead = pos->next;
		free(pos);
	}
	else {
		while (pcur->next != pos) {
			pcur = pcur->next;
		}
		pcur->next = pos->next;
		free(pos);
		pos = NULL;
	}
};

删除指定节点,只需要我们将前一个节点指向下一个节点就行了,然后再释放指定节点即可 。

而头删就是释放头节点,然后将头指针指向头节点的下一个节点既可以。

为了方便,头插头删位插尾删,我们都使用前面两个函数来进行完成,这样更加方便。

void SLTPushBack(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	if (*pphead == NULL) {
		*pphead = SLTBuyNode(x);
	}
	else {
		SLTInsert(pphead,NULL,x);
	}
}

在尾插时,如果链表为空,我们就直接创建一个新的节点即可,否则我们就只需要在NULL前面插入一个节点即可,因为最后一个节点的next指针指向的是空。

void SLTPushFront(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	if (*pphead == NULL) {
		*pphead = SLTBuyNode(x);
	}
	else {
		SLTInsert(pphead, *pphead, x);
	}
};

头插同理,如果链表为空直接创建新的链表即可,否则就是在头节点之前插入即可,而且头节点直接作为参数给我们使用了,所以我们就不需要再查找了. 

void SLTPopBack(SLTNode** pphead) {
    assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur->next != NULL) {
		pcur = pcur->next;
	}
	SLTErase(pphead,pcur);
};

尾删就需要我们遍历链表,找到最后一个链表的指针 ,这样再删除即可。链表为空就不能删了,所以我们要断言一下。

void SLTPopFront(SLTNode** pphead) {
    assert(pphead && *pphead);
	SLTErase(pphead,*pphead);
};

头删更简单,不需要遍历,直接调用删除函数即可。

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
    assert(pos);
	SLTNode* next = SLTBuyNode(x);
	next->next = pos->next;
	pos->next = next;
};
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos) {
    assert(pos);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
	next = NULL;
};

 这里是同理,要插入就直接创建新链表然后插入即可,删除也是同理,这里是需要判断pos是否为空的,因为如果没有找到find函数就会返回空。

删除链表

void SListDesTroy(SLTNode** pphead) {
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur) {
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
};

就需要遍历整个链表然后依次释放,最后为了防止野指针,我们还需要将*pphead置为NULL。 

三.链表 

链表的种类有很多种。

任意两两组合就有2 * 2 * 2 = 8种组合

这个带头和不带头后面双链表再讲解。

我们通常所说的单链表是不带头单向不循环链表。

单向就是通过一个只能访问下一个节点不能访问上一个节点,否则就是双向。

循环就是链表围成一个圈,链表的最后一个节点next指向头节点了,而不是NULL.

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构: 单链表 双向带头循环链表
  1.  ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
  2.  带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

 

  • 64
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
好的,我可以帮你讲解一下如何使用C++单链表来存储火车信息。 首先需要定义一个火车信息的结构体,例如: ```c++ struct Train { string name; // 火车名 string from; // 起点站 string to; // 终点站 string time; // 发车时间 }; ``` 接下来,定义一个单链表节点的结构体,其中包含一个火车信息的结构体和一个指向下一个节点的指针,例如: ```c++ struct Node { Train train; // 火车信息 Node* next; // 指向下一个节点的指针 }; ``` 定义一个单链表的类,其中包含头节点指针和尾节点指针,以及一些操作方法,例如: ```c++ class TrainList { public: TrainList() { head = new Node(); tail = head; } ~TrainList() { Node* current = head; while (current != nullptr) { Node* tmp = current; current = current->next; delete tmp; } } void insert(const Train& train) { Node* node = new Node(); node->train = train; tail->next = node; tail = node; } void remove(const Train& train) { Node* current = head->next; Node* prev = head; while (current != nullptr) { if (current->train.name == train.name && current->train.from == train.from && current->train.to == train.to && current->train.time == train.time) { prev->next = current->next; if (current == tail) { tail = prev; } delete current; break; } prev = current; current = current->next; } } void print() const { Node* current = head->next; while (current != nullptr) { cout << "火车名:" << current->train.name << endl; cout << "起点站:" << current->train.from << endl; cout << "终点站:" << current->train.to << endl; cout << "发车时间:" << current->train.time << endl; cout << endl; current = current->next; } } private: Node* head; Node* tail; }; ``` 然后在主函数中,可以创建一个TrainList对象,调用insert方法添加火车信息,调用remove方法删除火车信息,调用print方法打印所有火车信息,例如: ```c++ int main() { TrainList trainList; Train train1 = {"G1", "北京", "上海", "08:00"}; Train train2 = {"G2", "上海", "北京", "14:00"}; Train train3 = {"D1", "北京", "天津", "09:00"}; trainList.insert(train1); trainList.insert(train2); trainList.insert(train3); trainList.print(); trainList.remove(train1); trainList.print(); return 0; } ``` 以上就是使用C++单链表存储火车信息的示例,希望对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值