数据结构--单链表实现

0 阅读8分钟

链表

分别用C语言 C++实现了单链表

C语言

#include<stdlib.h>
//带头结点的单链表
typedef struct Node
{
	int data;
	struct Node* next;
}Node,*LinkList;
//Node 结构体类型
//LinkList 结构体指针类型 等价于Node*  链表头就用LinkList
//增强了可读性
Node* InitLinkList()
{
	Node* head = (Node*)malloc(sizeof(Node));
	if (head == NULL)
	{
		printf_s("初始化失败\n");
		return NULL;
	}
	head->next = NULL;
	return head;
}
//头插法
LinkList Head_Insert(LinkList L, int k)
{
	Node* s = (Node*)malloc(sizeof(Node));
	if (s == NULL)
	{
		printf_s("内存分配失败\n");
		return NULL;
	}
	s->next = L->next;
	L->next = s;
	s->data = k;
	return L;
}
//尾插法
LinkList Rear_Insert(LinkList L, int k)
{
	Node* s = (Node*)malloc(sizeof(Node));
	if (s == NULL)
	{
		printf_s("内存分配失败\n");
		return NULL;
	}
	s->data = k;
	Node* p = L;
	while (p->next != NULL)
	{
		p = p->next;
	}
	//p此时指向尾结点
	s->next = p->next;//s->next = NULL
	p->next = s;
	return L; 
}
//查找 找到返回结点地址 没找到返回NULL
Node* Find(LinkList L, int x)
{
	Node* p = L->next;
	while (p != NULL && p->data != x)
	{
		p = p->next;
	}
	//跳出循环1:p==NULL x不存在
	//跳出循环2:p不为空 p->data == x找到了x
	return p;
}
//在指定位置插入
LinkList Insert(LinkList L,int x, int k)
{
	Node* s = (Node*)malloc(sizeof(Node));
	if (s == NULL)
	{
		printf_s("内存分配失败\n");
		return NULL;
	}
	s->data = k;
	Node* p = Find(L, x);
	if (p == NULL)
	{
		printf_s("%d不存在\n", x);
	}
	else 
	{
		s->next = p->next;
		p->next = s;
	}
	return L;
}
//删除
//LinkList Delete(LinkList L, int k)//双指针
//{
//	Node* p = L->next;
//	Node* q = L;
//	while (p != NULL && p->data != k)
//	{
//		p = p->next;
//		q = q->next;
//	}
//	if (p == NULL)
//	{
//		printf_s("%d不存在无法删除\n", k);
//	}
//	else
//	{
//		q->next = p->next;
//		free(p);
//		p = NULL;
//	}
//	return L;
//}
//单指针
LinkList Delete(LinkList L, int k)
{
	Node* p = L;
	while (p->next != NULL && p->next->data != k)
	{
		p = p->next;
	}
	if (p->next == NULL)
	{
		printf_s("%d不存在无法删除\n", k);
	}
	else
	{
		Node* q = p->next;
		p->next = p->next->next;
		free(q);
		q = NULL;
	}
	return L;
}

void PrintList(LinkList L)
{
	Node* p = L->next;  // 跳过头结点
	printf("链表元素:");
	while (p != NULL)
	{
		printf("%d ", p->data);
		p = p->next;
	}
	printf("\n");
}
//遍历输出链表
int main()
{
	LinkList L = NULL;
	L = InitLinkList();

	// 头插
	L = Head_Insert(L, 6);
	L = Head_Insert(L, 7);
	L = Head_Insert(L, 4);
	L = Head_Insert(L, 0);
	PrintList(L);  // 0 4 7 6

	// 尾插
	L = Rear_Insert(L, 5);
	L = Rear_Insert(L, 9);
	L = Rear_Insert(L, 2);
	PrintList(L);  // 0 4 7 6 5 9 2

	// 在 7 后面插入 88
	Insert(L, 7, 88);
	PrintList(L);

	// 删除 5
	Delete(L, 5);
	PrintList(L);

	return 0;
}

C语言依旧选择用结构体来封装链表节点,链表节点包括数据域和指针域.数据域用int data来记录,而指针域则选择用结构体指针来记录,只需要按照所记录的地址就能找到下个节点.这里要注意虽然我们声明结构体的时候用了typedef来给他取一个别名,但是编译器是顺序阅读代码,此时我们仍应该用完整的stuct Node*来记录地址.我们给结构体取了两个别名分别是 Node *LinkList,是用来区别普通节点与头节点,LinkList是等价于Node*的.

链表的增删改查相较于顺序表逻辑会有所复杂,主要是因为链式结构不能像顺序结构一样直接在连续的地址上进行遍历.我们需要通过节点的指针域对整个链表进行遍历.

1.初始化直接malloc一个头节点出来,然后检查是否为空以此判断是否申请成功.我们只初始化了一个头节点所以head->next我们得置空,表示链表结束(无后续结点).

2.头插法我们先malloc一个新节点出来,因为是普通节点我们直接用Node*接收malloc的内存地址.malloc后常规操作判空.我们先将头节点后的所有节点全部接到新节点s后,再将s放置到头节点后便完成了头插的操作.再将s的数据域更新为k.有些时候我们会把链表的长度封装进结构体,如果这样的话头插后需要将s.size++.

3.尾插法需要对链表进行一个遍历,我们无法像顺序表一样直接找到链表的尾节点.同样malloc一个新节点然后判空,然后更新数据域.遍历链表我们先用一个p指针指向头节点,然后通过while循环遍历,如果p节点的下一个节点不为空说明并没有遍历到底,就让p指针后移即可.while循环结束后p就指向尾节点,先将s节点接入尾节点后然后让s节点后继置空.最后返回头节点即可.

4.查找函数我们也要对链表进行遍历,这次用p指针指向L->next因为头指针是没有数据域的我们无需遍历.然后while循环进行遍历.这个while循环的条件非常细节 (p != NULL && p->data != x) 这个顺序我们不能更改,因为编译器在判断循环条件时会先判断前面的条件,如果前面的条件不满足那么就会直接跳出循环,如果反过来的话会先访问节点的数据域而不是先判空,但是如果访问的时空指针的数据域就会导致程序崩溃.循环结束后p指针会指向目标节点如果没找到则是NULL.

5.指定位置插入需要配合Find函数找到目标p节点,先找到要插入的位置然后再插入.如果Find返回NULL那么要插入的位置不存在,反之先将p节点的后继接到s后然后再把s接到p后便完成了指定插入的操作.

6.删除操作我们需要找到要删除的节点以及他的前一个节点.我们可以用双指针 单指针 或者配合Find().这里用单指针代码会更简洁明了一些,但逻辑是一样的.我们依然用指针p指向头节点,然后对链表进行遍历.

(p->next != NULL && p->next->data != k)

如果p的后继不为空且他的数据不是目标数据那么就让p后移,循环结束后两种情况如果p == NULL就是没有找到我们打印提示后直接返回头节点即可,反之找到了目标节点的前一个节点.删除操作先要用指针q记录要删除的节点因为我们最后是需要释放这个内存的,把p的后继改为q的后继就将要删除的节点删掉了,此时q节点是独立于链表外的.我们需要free掉p然后再将他置空.

7.遍历打印就非常常规,直接逐个遍历逐个打印即可.

链式结构的遍历和顺序结构还是有很大的区别,链表的增删改查也得注意顺序不然很容易导致操作失败程序崩溃.逐个分析每个函数让我对链表的操作有了更深的理解.

C++

#include<iostream>
using namespace std;

class LinkList{
private:
	struct Node{
		int data;
	    Node* next;
	};
	Node* head;//头节点

public:
	//构造函数代替初始化函数
	LinkList() {
		head = new Node;
		head->next = nullptr;
	}
	// 头插法
	void Head_Insert(int k) {
		Node* p = new Node;
		if (p == nullptr) {
			cout << "内存申请失败" << endl;
			return;
		}
		p->next = head->next;
		head->next = p;
		p->data = k;
		return;
	}
	// 尾插法
	void Rear_Insert(int k) {
		Node* p = new Node;
		if (p == nullptr) {
			cout << "内存申请失败" << endl;
			return;
		}
		Node* t = head;
		while (t->next != nullptr) {
			t = t->next;
		}
		p->next = nullptr;
		t->next = p;
		p->data = k;
		return;
	}
	// 查找:返回节点指针(仍在类内部使用,外部看不到 Node)
	Node* Find(int x) {
		Node* t = head->next;
		while (t != nullptr && t->data != x) {
			t = t->next;
		}
		if (t == nullptr) {
			cout << "未找到目标节点" << endl;
			return nullptr;
		}
		else {
			return t;
		}
	}
	// 指定值后插入
	void Insert(int x, int k) {
		Node* p = Find(x);
		if (p == nullptr) {
			cout << "没找到位置" << endl;
			return;
		}
		Node* t = new Node;
		t->next = p->next;
		p->next = t;
		t->data = k;
		return;
	}
	// 删除(使用 Find 实现)
	void Delete(int k)
	{
		Node* p = Find(k);
		if (!p)
		{
			cout << k << " 不存在无法删除" << endl;
			return;
		}

		// 找前驱
		Node* pre = head;
		while (pre->next != p)
			pre = pre->next;

		pre->next = p->next;
		delete p;
	}

	// 遍历输出
	void PrintList()
	{
		Node* p = head->next;
		cout << "链表元素:";
		while (p)
		{
			cout << p->data << " ";
			p = p->next;
		}
		cout << endl;
	}

	// 析构函数:释放内存
	~LinkList()
	{
		Node* p = head;
		while (p)
		{
			head = head->next;
			delete p;
			p = head;
		}
	}
};

int main() {
	// 创建链表对象(自动调用构造函数初始化)
	LinkList L;

	// 1. 头插 4 个元素
	cout << "===== 头插法插入 =====" << endl;
	L.Head_Insert(6);
	L.Head_Insert(7);
	L.Head_Insert(4);
	L.Head_Insert(0);
	L.PrintList();  // 输出:0 4 7 6

	// 2. 尾插 3 个元素
	cout << "\n===== 尾插法插入 =====" << endl;
	L.Rear_Insert(5);
	L.Rear_Insert(9);
	L.Rear_Insert(2);
	L.PrintList();  // 输出:0 4 7 6 5 9 2

	// 3. 在 7 后面插入 88
	cout << "\n===== 在 7 后插入 88 =====" << endl;
	L.Insert(7, 88);
	L.PrintList();

	// 4. 删除元素 5
	cout << "\n===== 删除 5 =====" << endl;
	L.Delete(5);
	L.PrintList();

	// 5. 测试删除不存在的元素
	cout << "\n===== 测试删除不存在的 100 =====" << endl;
	L.Delete(100);

	return 0;
}

在C++中将整个链表节点全部封装进一个类,构造函数代替初始化函数,同时我们加入了手写的析构函数来逐个释放内存.整体的逻辑相同,在Delete函数部分我选择使用Find函数实现先找到目标节点,再遍历找到他的前继节点,后续操作相同.

写C++用class封装是为了更好的体会CPP面向对象的思想.相较于C语言面向过程的编程,cpp的逻辑会在代码中体现的更加清晰.