数据结构之线性表--链式描述

概述

在线性表的链式描述中,线性表的元素在内存中的位置是随机的。每个元素都有一个明确的指针或链(指针和链是一个意思)指向线性表的下一个元素的位置。

单向链表

描述

在链式描述中,数据对象实例的每一个元素都用一个单元或节点来描述。节点不必是数据成员,因此不是用公式来确定元素的位置。取而代之的是,每一个节都明确包含另一个节点相关的位置信息,这个信息成为链或者指针。
每一个节点都有一个链域,它的值是线性表下一个元素的位置,即地址。这样一来,ei的节点链接着元素ei+1的节点。最后一个元素的节点没有其他的节点可以连接,因此链域的值为NULL。变量firstNode用来指向链式描述的第一个节点。所以,在线性表的链式描述中,想要找到ei的位置,就必须从e1开始逐个寻找,不能像数组描述中一样,用下标索引进行寻找。
在链式描述中,每一个节点只有一个链,这种结构成为单向链表。链表从左到右(最后一个节点除外)都连接着下一个节点,最后一个节点的链域值为NULL。这样额结构也称为链条。

结构chainNode

为了用链表描述线性表,我们要定义一个结构chainNode和一个类chain。

#include<iostream>
using namespace std;
template<class T>
struct chainNode 
{
	T element;
	chainNode<T>* next;
	chainNode(){ }
	chainNode(const T& element)
		{this->element = element;}
	chainNode(const T& element,chainNode<T>* next)
	{this->element = element;this->next = next;}
};

类chain

类chain用单向链表实现了线性表,其最后一个指针域的节点为NULL ,即它用单向连接的一组节点实现线性表。

template<class T>
class chain
{
public:
	chain(int initailCapacity = 10);
	chain(const chain<T>& );
	~chain();
	bool empty(){return listSize = 0;}
	int size() const {return listSize;}
	T& get(int theIndex) const ;
	int indexOf(T& theElement ) const;
	void erase(int theIndex);
	void Insert(int theIndex,const T& theElement);
	void output(ostream& out) const;
protected:
	void checkIndex(int theIndex) const;
	chainNode<T>* firstNode;
	int listSize;
};

数据成员firstNode和listSize。firstNode是线性表指向首元素(即线性表第零个元素的节点)的指针。当链表为空的时候firstNode的值为NULL,listSize表示线性表的元素个数,它等于链表的节点数。

构造函数和赋值构造函数

为了创建一个空链表,只需令第一个节点的指针firstNode 的值为NULL,与数组描述的线性表不同,链表在创建的时候不需要估计元素的最大个数以分配初始空间。不过构造函数还是具有一个表示初始容量的形参initialCapacity,目的是为了和arrayList相容。
下面展示一些 内联代码片

template<class T>
chain<T>::chain(int initialCapacity)
{
	if (initialCapacity<1 )
	{
		cerr<<"wrong!wrong!wrong!"<<endl;
	}
	firstNode = NULL;
	listSize = 0;
}
template<class T>
chain<T>::chain(const chain<T>& theList)
{
	listSize = theList.listSize;
	if(listSize == 0)
	{
		firstNode = NULL;
		return ;
	}
	else
	{
		chainNode<T>* sourceNode = theList.firstNode;
		firstNode = new chain<T>(sourceNode->element);
		sourceNode = sourceNode->next;
		chainNode<T>* targetNode = firstNode;
		while(sourceNode!=NULL)
		{
			targetNode->next = new chainNode<T>*(sourceNode->element);
			targetNode = targetNode->next;
			sourceNode = sourceNode->next;
		}
		targetNode->next = NULL;
	}
}

其中构造函数相比于复制构造函数好理解,下面我将会详细的解释一波复制构造函数的运行原理:
在这里插入图片描述

析构函数

析构函数是要逐个清除链表的节点。实现的策略是重复清除链表的首元素节点,直到链表为空。注意,我们在清除首节点的时候必须要用nextNode保存第二个元素节点的指针。
下面展示一些 内联代码片

template <class T>
chain<T>::~chain()
{
	while(firstNode!= NULL)
	{
		chainNode<T>* nextNode = firstNode->next;
		delete firstNode;
		firstNode = nextNode;
	}
}

析构函数相对更好理解撒。

方法get

在数组描述的线性表中,我们根据公式来计算一个表元素的位置。然而在链表中,要寻找索引为theIndex的元素,跟踪链域next直到找到所需要的的元素节点指针,也就是说必须跟踪theIndex个指针。
下面展示一些 内联代码片

template<class T>
T& chain<T>::get(int theIndex) const
{
	chainNode<T>* currentNode = firstNode;
	for(int i = 0;i < theIndex ;i++)
	{
		currentNode = currentNode->next;
	}
	return currentNode->element;
}

方法indexOf

返回元素首次出现时的索引

template<class T>
int chain<T>::indexOf(T& theElement) const
{
	chainNode<T>* currentNode = firstNode;
	int index = 0;
	while(currentNode != NULL && currentNode->element != theElement)
	{
		currentNode = currentNode->next;
		index++;
	}
	if(currentNode == NULL)
	{
		return -1;
	}
	else
	{
		return index;
	}
} 

这个算法也是很好理解的。

方法erase

从链表中删除索引为theIndex 的元素,本质上就是让被删除元素的前一个节点的next指针直接指向被删除元素后一个节点,然后再把要删除的节点空间释放。需要注意的是,和析构链表一样,我们需要把被删除节点中的next指针留存下来,因为被删除节点中的next指针指向的是被删除的节点后一个节点,我们要将这个指针赋给被删除链表的前一个节点的next值。

template<class T>
void chain<T>::erase(int theIndex)
{
	chainNode<T>* deleteNode ;
	if(theIndex == 0)
	{
		deleteNode = firstNode;
		firstNode = firstNode->next;
	}
	else
	{
		chainNode<T>* p = firstNode;
		for(int i = 0;i<theIndex-1;i++)
		{
			p=p->next;
		} 
		deleteNode = p->next;
		p->next = p->next->next;
	}
	listSize--;
	delete deleteNode;
}

上面展示的代码主要的思路是这样的:要删除的那个节点的地址在要删除节点的前一个节点的next指里,所以我们析构前一个节点的next指针。然后再把要删除节点的next指针赋给前一个指针就可以完成节点的删除操作。所以,我们应该定义一个deleteNode用来暂时储存前一个节点的next指针,这个时候前一个节点的地址就会有两个备份,一份在deleteNode里,另一份在next里。然后再用要被删除的节点的next指针给前一个next指针赋值(这个时候deleteNode中的地址没变,前一个next指针的地址被改成了指向被删除元素后面一个节点的地址),然后再析构deleteNode,就是释放了地址deleteNode指向的那一片要被删除的空间。

方法insert

删除和插入的操作过程很相似,为了在链表中索引为theIndex的位置上插入一个新元素,需要首先找到索引为theIndex-1的元素节点,然后在该节点之后插入新元素节点。
这个算法相对理解起来相对较困难,我会比较详细的介绍:
下面展示一些 内联代码片

template<class T>
void chain<T>::Insert(int theIndex,const T& theElement )
{
	if(theIndex< 0||theIndex > listSize)
	{
		cerr<<"wrong!wrong!wrong!"<<endl;
	}
	else 
	{
		if(theIndex == 0)
		{
			firstNode = new chainNode<T>*(theElement,firstNode);
		}
		else 
		{
			chainNode<T>* p  = firstNode;
			for(int i=0;i<theIndex-1;i++)
			{
				p = p->next;
			}
			p->next = new chainNode<T>*(theElement,p->next);
		}
	}
	listSize++;
}

解析:在链表中的某个索引位置上插入一个节点,例如在索引3的位置上插入一个新节点取代原来在索引3的节点,并把原来在索引3 的节点挤到索引为4的位置。那么一旦操作完成,节点2的next指针就会储存新加入的节点的地址,而新加入节点的next指针会储存插入前节点3的地址。相对应的操作是:首先我们要调用产Node的构造函数生成一个新节点。在新节点中我们把next指针赋成原来节点2中的next指针,再把新元素放到新节点的element域中。然后将这个新生成的节点的地址赋给节点2的next指针上(这个操作我们用new来完成。)
相对应的代码就是
p->next = new chainNode*(theElement,p->next);
这样我们就完成了向链表中插入一个新节点的操作。

输出链表

输出链表就是将链表插入输出流(有的时候会用到流插入运算符的重载)
下面展示一些 内联代码片

template<class T>
void chain<T>::output(ostream& out) const
{
	for(chainNode<T>* currentNode =  firstNode;
					  currentNode != NULL;
					  currentNode =  currentNode->next)
		out<<currentNode->element<<" ";
}
template<class T>
ostream& operator<<(ostream& out,const chain<T>& x)
{
	x.output(out);
	return out;
}

以上就是线性表的链式描述的一些内容,我们再来回顾一下:
如何定义出一个链表;
链表的构造函数
链表的析构函数
链表的复制构造函数
查找索引为index的元素
查找元素第一次出现时的索引
如何在链表中删除一个元素
如何在链表中插入一个元素
将链表链表插入输出流

循环链表和头结点

将链表描述成一个单项循环链表,要在链表的前面增加一个节点(用到了单链表的插入操作,上文有介绍),成为头结点,将链表的尾节点和头结点连接起来(用尾节点的null指针赋上headerNode),单向链表就会变成一个循环链表。(代码就不展示了,操作都是已经展示过的)

双向链表

双向链表的每一个节点都有三部分构成:
1.指向前驱的指针
2.指向后继的指针
3.元素域
可以双向的访问历遍。

后记

以上就是线性表链式描述的的一些已基本算法,希望能对你有所帮助。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

元解~殇怀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值