2-2 线性表之链表 及其C++实现

 更多系列博文请点击:0-数据结构与算法链接目录 

2-2 线性表之链表 及其C++实现

采用顺序存储结构的顺序表,其数据元素是用一组地址连续的存储单元来依次存放的,无须为表示数据元素之间的逻辑关系而增加额外的存储空间,其逻辑关系蕴含在存储单元的邻接关系中,并且可以方便地随机存取表中的任一元素,但是从它的插入和删除算法可以看出,顺序表的效率较低,需要大量的数据元素的移位。同时,数据元素最大个数需要预先确定,这使得计算机存储器使用率也不高。

而采用链式存储结构的链表是用一组任意的存储单元来存放线性表的数据元素,这组存储单元既可以是连续的也可以是不连续的,甚至可以零散分布在内存中的任何位置上,从而大大提高存储器的使用效率。

1.单向链表

链表,别名链式存储结构或单链表,是链式存储结构中最简单和最基本的结构,与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。在存储每个元素的同时,需要存储其直接后继(或直接前驱)的位置!这一部分称为:链

因此单向链表的每个元素都由两部分组成,存储元素的数据域data,和存储直接后继元素地址的指针域next。举例:

struct LNode

{

int data; //姑且认为其数据为整型

struct LNode * next;

}linklist_node;

 

一个线性表 a1,a2,...,an的对应单链表逻辑示意图如下,

其中h是链表的头指针,用以确定线性表中的第一个元素对应的存储位置,单链表可以用头指针的名字来命名,链表终点元素无直接后继,指针域为null空。用^表示。

为了实现算法上的方便,通常在单链表的第一个元素之前附加一个称为头结点的元素,头结点的数据域可以不存储任何数据,也可以存储像线性表的表长那样的数据信息,但一般都不存数据。h这时就是头结点的存储位置!

示意图如下:

对于链表来说,头结点不是必须的,它的作用只是为了方便解决某些实际问题;

头结点的数据域可以不存储任何信息,头结点的指针域存储指向第一个结点的指针(即第一个元素结点的存储位置)。头结点的作用是使所有链表(包括空表)的头指针非空,把空表和非空表的处理统一起来了,并使对单链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致。

比如说如果要删除第一个元素,没有头结点的链表,第一个元素的位置就是h,删除第一个元素之后,h指针就得更新为原来的第二个元素的位置;但是对于有头结点的单链表,由于h指针指向的是头结点,所以删除第一个位置的元素和删除其他位置的元素的操作都是一样的,不需要再更新h指针!

①下面介绍带头结点的单链表的操作集合:

 

 

头文件:link_list_with_headnode.h

#ifndef LINK_LIST_HEAD_NODE_H_
#define LINK_LIST_HEAD_NODE_H_

typedef struct Link_List_Node {
	int data; //数据域
	struct Link_List_Node * next; //指针域
}node;

/*基本操作*/
void Init(node **h);
void Create(node **h);

int Len(node *h);
int Get(node *h, int i);
int Locate(node *h, int x);

void Insert(node *h, int i, int x);
void Delete(node *h, int i);


void Show(node *h);
void Destory(node **h);

#endif

函数定义文件: link_list_with_headnode.cpp

我把需要讲解的地方都写在注释里面了,可能csdn这个显示的灰色注释不太好看,

可以复制到VS中,这样看起来清楚一些

但我还是单独讲一下几个操作:

a初始化操作:

/*初始化一个空链表*/
void Init(node **h) {
    (*h) = new node;//分配一个内存空间,把地址返回给(*h)
    (*h)->next = nullptr;//头结点的下一个位置为空,参考上面的图片
}

这里需要注意的是,传入的参数 h前面有两个星号*,本质上还是函数按指针传递的思路,

熟悉C语言的都知道,想要在函数中改变函数外部变量的值,如果只是按值传递是没有用的

函数只是改变了函数内部的局部变量的值,所以很多时候我们都传递指针,即变量的地址

依照这个地址去修改相应内存空间的值,就能改变函数外部的变量。

如果说我需要改变一个int a 的值,那么有可能我会传入  int * address(即int类型的指针),这个想必大家都理解

但是这里我们本来就是要改变外面的头指针,即要改变外面的一个指针变量的值,同样我们应该传递这个指针变量的指针,

就要用两个星号*,所以可以这么理解,我们将两个星号分开来看,node *   *h.

前面的node * 表示的是node指针类型,你可以把它当做一个类型名,像是int,char之类的类型名,

第二个指针 则是表示 我的h变量是个指针变量

这样就是: h变量是个指针变量,所以有 *h,  然后又是指向 (node *) 这个类型的
所以有 node **h。

所以凡是需要修改头指针的地方,都需要传入指针变量的指针。

b 、Get函数

int Get(node *h, int i) {
    int j = 0;
    node *p = h;

    while (p->next != nullptr && j < i) {
        p = p->next;
        j++;
    }

    if (j != i) {
        cout << "\ninvalid index: " << i << " !";
        return NAN;
    }

    return p->data;
}

这个函数描述了,我如何在链表中找到第i个位置的元素,这个函数只要熟悉了,其实链表的插入 和 删除 也就好理解了。因为要插入到第 i  个位置,或 删除第i个位置,都需要找到 第 i -1 个元素的地址

所以后面你会看到插入和删除函数中,都有找第i-1个元素的地址这么一个操作,

跳出while循环的条件为2选1,只要有一个不满足,就跳出循环。

第一个是如果 p->next == nullptr, 表明p已经到了最后一个元素了,因为p的后面已经是null了,

第二个是如果 j = i的时候,就不满足 j<i这个条件了,也就跳出;

所以我本身希望的是它是因为第二个条件跳出的,这样我p也就是刚好到第i个元素的位置,

然后不一定我们输入的i就在合理范围内,比如假设总共有10个元素,但输入的i 却为100,

那么显然这时候就是因为遍历到了链尾而跳出,j肯定只能为10,

所以我们在后面加了一个判断语句,看看j是不是因为第二个条件才跳出的,如果 j != i,

说明并没有那么多个元素,index不合理。

#include<iostream>
#include"link_list_head_node.h"

using std::cin;
using std::cout;
using std::endl;

/*初始化一个空链表*/
void Init(node **h) {
	(*h) = new node;//分配一个内存空间,把地址返回给(*h)
	(*h)->next = nullptr;//头结点的下一个位置为空,参考上面的图片
}


/*求链表的长度*/
int Len(node *h) {
	int j = 0;
	node *p = h;
	while (p->next != nullptr) {
		p = p->next;
		j++;
	}
	return j;
}

/*返回指定位置的元素*/
int Get(node *h, int i) {
	int j = 0;
	node *p = h;

	while (p->next != nullptr && j < i) {
		p = p->next;
		j++;
	}

	if (j != i) {
		cout << "\ninvalid index: " << i << " !";
		return NAN;
	}

	return p->data;
}

/*找到某元素的位置*/
int Locate(node *h, int x) {
	node *p = h;
	int j = 0;
	while (p->next != nullptr) {
		p = p->next;
		j++;
		if (p->data == x)
			return j;
	}

	cout << "\nthere is no x in this list!\n";
	return -1;
}

/*在指定位置插入元素*/
//核心思想是找到第 i-1个元素,把新元素添加到i-1元素的后面,成为第i个元素
//所以中间的代码部分和 Get 函数几乎一致,只是要找的位置是i-1而已
void Insert(node *h, int i, int x) {
	int j = 0;
	node *p = h;

	/*这个循环是为了找到第i-1个元素的位置,
	可以看到与 Get函数相比,条件从 j<i 变成了 j<i-1 而已*/
	while (p->next != nullptr && j<i-1) {
		p = p->next;
		j++;
	}
	if (j != i - 1) {//如果跳出循环是j并不是i-1,说明停止时发生的条件是 p->next==nullptr;
		cout << "\ninvalid index: " << i << " ! \n";
		return;
	}

	/*如果通过了以上所有的检查,说明可以插入*/
	node *s = new node;//创建一个新节点
	s->data = x;//将x赋值给data域
	s->next = p->next;//链条修改
	p->next = s;

}
void Delete(node *h, int i) {
	int j = 0;
	node *p = h;
	//同样是要找第i-1个位置
	while (p->next != nullptr && j<i-1) {
		p = p->next;
		j++;
	}
	//这里多了一个条件是 要保证p->next!=nullptr,因为要确保第i-1个位置不是末位置
	//如果 p->next==nullptr,说明后面已经没有结点了,我们怎么去删除下一个位置上的 i 元素呢?
	if (j != i - 1 && p->next == nullptr) {
		cout << "\ninvalid index: " << i << " !\n";
		return;
	}

	node *s = p->next;//先将下一个元素的位置记录下来
	p->next = s->next;//p的next指向 i 元素的 下一个位置,即跳过 i位置
	delete s; //这一步很重要,由于是动态分配的内存,所以一定要及时收回!!!
}

/*根据输入创建链表*/
void Create(node **h) {
	cout << "\nPlease input the int number:  and use 'q' to quit! \n";

	int x;
	node *p, *s;

	/*初始化头结点*/
	(*h) = new node;
	(*h)->next = nullptr;

	/*位置指针指向头结点*/
	p = (*h);

	while (cin >> x) {

		/*采用尾插法,所以这个结点的next一定为null*/
		s = new node;
		s->data = x;
		s->next = nullptr;
		/*将位置指针指向尾部结点*/
		while (p->next != nullptr) 
			p = p->next;	
		p->next = s;//更新尾部结点

	}

	/*清除错误的输入流,因为我们设定的x是int类型,所以当输入非int类型是,cin>>x这一表达式会是 false!
	从而能够跳出上面的循环!但是错误类型的输入依然积累在输入流之中,为了不干扰后面的输入,就应该将其清除!*/
	cin.clear();
	while (cin.get() != '\n')
		continue;

}



void Show(node *h) {
	node *p = h;
	while (p->next != nullptr) {
		p = p->next;
		cout << p->data << "   ";
	}
	cout << endl;
}



void Destory(node **h) {
	node *p;

	while ((*h) != nullptr) {//如果当前头结点不为空,那就要删除当前结点
		p = (*h)->next;//先将当前头结点的下一个位置存起来备用
		delete (*h);//然后删除 头结点指针指向的内存空间
		(*h) = p;//然后将下一个位置当作新的头结点指针,如果下一个为空就跳出该循环了,如果不是,就继续循环
	}
	//这样一来就把所有节点都删除了
}

 

主函数文件:use.cpp

 

#include<iostream>
#include"link_list_head_node.h"

using std::cin;
using std::cout;
using std::endl;

int main() {

	node *list1, *list2;
	Init(&list1);
	cout << "\nlength of list1 is: " << Len(list1) << endl;

	/*用Insert方法依次在被初始化过的链表list1尾部插入新元素,以此创建链表*/
	
	cout << "\nPlease input the 8 numbers:\n";
	int x1;
	for (int k = 1; k < 9; k++) {
		cin >> x1;
		Insert(list1, k, x1);
	}
	cin.get();
	cout << "\nthe length of list1: " << Len(list1) << endl;
	Show(list1);


	/*也可以用我写的那个Create程序创建新链表,但是要注意一点:
	我那个程序是针对没有被初始化过的链表指针,因为那个函数里面有初始化语句,
	所以如果你输入一个已经被初始化过的链表,哪怕是空链表,的头指针,也会有个问题存在,
	那就是头指针的值被更新为 程序中使用 new创建的那个内存块的地址,但是你又没有释放原来头指针指向的内存块的地址,
	这样不符合程序规定,容易造成溢出, 所以应该使用没有被初始化过的链表指针,比如此程序中的list2*/
	Create(&list2);
	cout << "\nThe length of list2: " << Len(list2) << endl;
	Show(list2);


	/*删除函数测试*/
	cout << "\nnow test the Delete funtion, every turn we delete the 1st element of the list:\n";
	for (int i = Len(list2); i >0; i--) {
		Delete(list2, 1);
		Show(list2);
	}

	cin.get();
	Destory(&list1);
	Destory(&list2);
	return 0;
}

 

 

不带头结点的单链表

其实不带头结点的单链表,主要是在插入和删除的时候会有点麻烦,因为如果要在第一个元素位置进行

插入或者删除操作,由于没有头结点,头指针就是指向第一个元素的位置,那么势必头指针会改变,所以会

多几行操作更新头指针。

所以程序大致上相同,就是插入 Insert 和 删除 Delete 函数 需要像初始化那样 传入指针的指针,目的是可能会修改头指针

头文件:link_list_no_headnode.h

#ifndef LINK_LIST_NO_HEADNODE_H_
#define LINK_LIST_NO_HEADNODE_H_


typedef struct Link_list_node
{
	int data;
	struct Link_list_node * next;
}node ;

/*基本操作*/
void Init(node **h);
void Create(node **h);

int Len(node *h);
int Get(node *h, int i);
int Locate(node *h, int x);

void Insert(node **h, int i, int x);
void Delete(node **h, int i);


void Show(node *h);
void Destory(node **h);


#endif

函数定义文件:link_list_no_headnode.cpp

#include<iostream>
#include"link_list_no_headnode.h"

using std::cin;
using std::cout;
using std::endl;

/*不带头结点的单链表直接初始化头指针为空*/
void Init(node **h) {
	(*h) = nullptr;
}


void Create(node **h) {
	cout << "\nPlease input the int number:  and use 'q' to quit! \n";

	int x;
	node *p, *s;

	/*初始化头结点*/
	(*h) = nullptr;;
	/*位置指针指向头结点*/
	p = (*h);
	while (cin >> x) {

		/*采用尾插法,所以这个结点的next一定为null*/
		s = new node;
		s->data = x;
		s->next = nullptr;

		if (p == nullptr) {//建立第一个结点时,p是头指针的值,即null
			p=(*h) = s;

			continue;
		}
		
		/*将位置指针指向尾部结点*/
		while (p->next != nullptr)
			p = p->next;
		p->next = s;//更新尾部结点

	}

	/*清除错误的输入流,因为我们设定的x是int类型,所以当输入非int类型是,cin>>x这一表达式会是 false!
	从而能够跳出上面的循环!但是错误类型的输入依然积累在输入流之中,为了不干扰后面的输入,就应该将其清除!*/
	cin.clear();
	while (cin.get() != '\n')
		continue;

}

int Len(node *h) {
	int j = 0;
	node *p = h;
	while (p != nullptr) {
		j++;
		p = p->next;
	}

	return j;
}


int Get(node *h, int i) {
	
	node *p = h;
	if (p == nullptr) {
		cout << "\nThe is an empty list!\n";
		return NAN;
	}
	/*如果头指针不为空,说明至少有1个元素*/
	int j = 1;
	while (p->next!=nullptr && j<i){
		j++;
		p = p->next;
	}

	if (j != i) {
		cout << "\ninvalid index: " << i << " ! \n";
		return NAN;
	}
	return p->data;
}


int Locate(node *h, int x) {
	node *p = h;
	int j = 0;
	while (p != nullptr) {
		j++;
		if (p->data == x)
			return j;
		p = p->next;	
	}
}

void Insert(node **h, int i, int x) {

	/*不管是不是空链表,都能在第一个元素位置插值,
	而且因为要改变头指针,所以拿出来单独讨论*/
	if (i == 1) {
		node *s = new node;
		s->data = x;

		if ((*h) == nullptr) {//如果链表为空,则s的下一个位置为空
			s->next = nullptr;
			(*h) = s;//将s的位置赋值给头指针
		}

		else {//如果链表不为空,则s的下一个为原来的头指针
			s->next = (*h);
			(*h) = s;//将s的位置赋值给头指针
		}
		return;
	}
	else {
		if ((*h) == nullptr) {//如果不是在第一个位置插,而链表又为空,显然不合理
			cout << "\ninvalid index: " << i << " ! \n";
			return;
		}
		//下面和带头结点的头指针就一样了,因为我们这里是不在第一个位置插值,相当于第一个位置固定了,相当于头结点
		int j = 1;
		node * p = (*h);
		while (p->next != nullptr&&j<i-1) {
			j++;
			p = p->next;
		}
		if (j != i - 1) {
			cout << "\ninvalid index: " << i << " ! \n";
			return;
		}
		node *s = new node;
		s->data = x;
		s->next = p->next;
		p->next = s;

	}
}

void Delete(node **h, int i) {
	/*若为空,直接退出*/
	if ((*h) == nullptr)
		return;

	/*如果要删除第一个元素,则需要修改头指针,单独讨论*/
	if (i == 1) {
		node *p = (*h)->next;
		delete (*h);//释放头指针指向的内存
		(*h) = p;//更新头指针
		return;
	}

	//后面跟带头结点的也一样了
	int j = 1;
	node *p = (*h);
	while (p->next != nullptr &&j < i-1) {
		j++;
		p = p->next;
	}

	if (j != i - 1 && p->next == nullptr) {
		cout << "\ninvalid index: " << i << " ! \n";
		return;
	}
	node *s = p->next;
	p->next = s->next;
	delete s;

}


void Show(node *h) {
	node *p = h;
	while (p != nullptr) {
		cout << p->data << "   ";
		p = p->next;
	}
	cout << endl;
}

void Destory(node **h) {

	node *p;
	while ((*h) != nullptr) {
		p = (*h)->next;
		delete (*h);
		(*h) = p;
	}
}

主函数文件:use.cpp

#include<iostream>
#include"link_list_no_headnode.h"

using std::cin;
using std::cout;
using std::endl;

int main() {

	node *list1, *list2;
	Init(&list1);
	cout << "\nlength of list1 is: " << Len(list1) << endl;

	/*用Insert方法依次在被初始化过的链表list1尾部插入新元素,以此创建链表*/

	cout << "\nPlease input the int number:\n";
	int x1;
	for (int k = 1; k < 9; k++) {
		cin >> x1;
		Insert(&list1, k, x1);
	}
	cin.get();
	cout << "\nthe length of list1: " << Len(list1) << endl;
	Show(list1);


	/*也可以用我写的那个Create程序创建新链表,但是要注意一点:
	我那个程序是针对没有被初始化过的链表指针,因为那个函数里面有初始化语句,
	所以如果你输入一个已经被初始化过的链表,哪怕是空链表,的头指针,也会有个问题存在,
	那就是头指针的值被更新为 程序中使用 new创建的那个内存块的地址,但是你又没有释放原来头指针指向的内存块的地址,
	这样不符合程序规定,容易造成溢出, 所以应该使用没有被初始化过的链表指针,比如此程序中的list2*/
	Create(&list2);
	cout << "\nThe length of list2: " << Len(list2) << endl;
	Show(list2);


	/*删除函数测试*/
	cout << "\nnow test the Delete funtion, every we turn delete the 1st element of the list:\n";
	for (int i = Len(list2); i >0; i--) {
		Delete(&list2, 1);
		Show(list2);
	}

	cin.get();
	Destory(&list1);
	Destory(&list2);
	return 0;
}

 

如有错误,望不吝指出,谢谢!

 更多系列博文请点击:0-数据结构与算法链接目录 

 

 

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值