用C++来KO链表!!!

有一种很常用的数据结构叫链表,他简直就是孙悟空,七十二变还觉得少,单向链表,单向循环链表,双向链表,双向循环链表。。。。而且还有带头结点的、不带头节点的。果然是纷繁复杂的世界,不管怎样,先从最简单的开始。
一张粗图胜过千言万语,用图解的方式讲解数据结构是最好的方法。下图就是一个带头结点的单向链表。

在这里插入图片描述

创建一个带头结点的链表很简单,结点使用结构体定义,里面放数据元素和一个指向后继结点的指针,但是创建好的链表好像没啥用,什么操作都没有。没有那就加上嘛,增删查改是最基本的操作,现在就来逐个实现咯。
用图来说明单向链表的插入操作,简单明了啊!

在这里插入图片描述

具体用例子说明,把前驱结点1的next指针赋值为要插入的结点X,当前结点X的next指针就指向2结点。就这样完事了。
插入结点完成了,那就到删除结点了。还是拿图来说明吧,一看就懂。
在这里插入图片描述
在这里插入图片描述

总的来说就两步操作。
查找操作就没有什么图解的说法,直接循环比较就是了。但有一点需要特别强调的是,如果对单链表的遍历每次都从头节点出发,效率就会大大降低,特别是在频繁查找和获取某一结点的数据元素的场合,更是如此。所以,提供一组遍历函数,使用游标的方式来遍历链表。以后链表的遍历就很简单了。

上代码:

#ifndef LINKLIST_H
#define LINKLIST_H

template <typename T>
class LinkList
{
protected:
	struct Node
	{
		T data;
		Node* next;	
	};
	
	int m_length;
	int m_step;		// 使用游标遍历的步长
	mutable Node header;	// 头节点,使用mutable声明兼容const对象
	Node* m_current;	// 游标
	
	LinkList(const LinkList<T>& obj);	// 作为容器类使用
	LinkList<T>& operator = (const LinkList<T>& obj);
public:
	LinkList()	// 初始化工作要做好
	{
		header.next = NULL;
		m_current = NULL;
		m_length = 0;
		m_step = 0;
	}

	bool insert(const T& value)	// 尾插法,直接插入链表的尾部
	{
		return insert(m_length,value);
	}
	
	bool insert(int i, const T& value)
	{
		bool ret = (0 <= i) && (i <= m_length);
		
		if( ret )
		{
			Node* n = new Node();
			n->data = value;
			Node* current = &header;	// 获取前驱结点
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			if( current == &header )	// 头插
			{
				n->next = header.next;	// 两步操作
				header.next = n;
			}
			else
			{
				n->next = current->next;// 两步操作
				current->next = n;
			}
			
			++m_length;
		}
		else
		{
			throw(0);
		}
		
		return ret;
	}
	
	bool remove(int i)
	{
		bool ret = (0 <= i) && (i < m_length);
		
		if( ret )
		{
			Node* current = &header;
			for(int j = 0; j < i; ++ j)	// 找到要删除的结点的前驱结点
			{
				current = current->next;
			}
			
			Node* todel = current->next;	// 要删除的结点
			
			if( m_current == todel )	/如果游标正好处在被删除结点的位置,则移动游标
			{
				m_current = m_current->next;
			}
			
			current->next = todel->next;	// 指向要删除结点的后继结点
			
			--m_length;
			delete todel;
		}
		else
		{
			throw(0);
		}
		
		return ret;
	}
	
	int find(const T& obj) const
	{
		int ret = -1;
		int i = 0;
		Node* current = &header;
		
		while( current != NULL )	// 从头到尾遍历
		{
			if( current->data== obj )
			{
				ret = i;
				break;		// 找到第一个就返回
			}
			
			++i;
			current = current->next;
		}
		return ret;
	}
	
	bool set(int i, const T& obj)
	{
		bool ret = (0 <= i) && (i < m_length);
		
		if( ret )
		{
			Node* current = header.next;	// 找当前结点
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			current->data = obj;
		}
		else
		{
			throw(0);
		}
		return ret;
	}
	
	T get(int i) const
	{
		T ret;
		
		if( !get(i, ret) )	// 获取失败,抛出异常
		{
			throw(0);
		}
		
		return ret;
	}
	
	bool get(int i , T& value) const
	{
		bool ret = (0 <= i) && (i < m_length);
		
		if( ret )
		{
			Node* current = header.next;
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			value = current->data;
		}
		
		return ret;
	}
	
	int length() const
	{
		return m_length;
	}
	
	// 提供遍历函数,使用方便并且很高效;
	bool move(int i, int step = 1)	// 默认步长为1
	{
		bool ret = (0 <= i) && (i < m_length);
		
		if( ret )
		{
			m_current = header.next;
			for(int j = 0; j < i; ++ j)
			{
				m_current = m_current->next;
			}
			
			m_step = step;
		}
		
		return ret;
	}
	
	bool end()	// 判断是否到达链表尾部
	{
		return (m_current == NULL);
	}
	
	bool next()	// 按步长移动游标
	{
		int i = 0;
		
		while( (i < m_step) && !end() )
		{
			m_current = m_current->next;
			++i;
		}
		
		return (i == m_step);
	}
	
	T current()	// 返回游标当前位置的数据元素
	{
		if( !end() )
		{
			return m_current->data;
		}
	}
	
	void clear()	// 清空链表的操作
	{
		while( m_length > 0 )
		{
			remove(0);
		}
	}
	
	~LinkList()
	{
		clear();
	}
};

#endif

好长啊!先来个小测试。

#include "LinkList.h"
using namespace std;

int main(int argc, const char* argv[])
{
	LinkList<int> list;
	
	for(int i = 0; i < 10; ++ i)
	{
		list.insert(i);	// 采用尾插,顺序是从小到大
	}
	
	for(list.move(0); !list.end(); list.next())
	{
		cout << list.current() << endl;
	}
	
	return 0;
}

在这里插入图片描述

单链表很好用,也经常使用,不过它没有什么特别。如果把最后一个结点的指针指向第一个结点,那么就得到一个环,这个环可以用来解决一个很著名的问题,约瑟夫环,故事的开端很有趣,某一天,历史学家约瑟夫和他的朋友为了躲避战乱,逃到一个山洞,山洞里有39个犹太人,但这些犹太人决定宁愿死也不要被敌人抓到,于是想出了一个可怕的自杀方式,41个人围成一个圈圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而约瑟夫和他朋友可不想死啊,但又不敢反抗,毕竟人家人多势众!那只能靠智商了,只要找出最后自杀的两个位置就可以避免惨剧发生了。靠智商来救命,可能绝大部分人都会挂。然鹅,使用单向循环链表就可以轻松解决这个问题,保住性命!
现在就来实现循环的单链表。看图码字。

在这里插入图片描述

扫描二维码关注公众号,回复: 11317460 查看本文章

#ifndef CIRCLELIST_H
#define CIRCLELIST_H

template <typename T>
class CircleList
{
protected:
	struct Node
	{
		T data;
		Node* next;
	};
	int m_length;
	int m_step; 
	Node* m_current;
	mutable Node header;
public:
	CircleList()
	{
		header.next = NULL;
		m_current = NULL;
		m_length = 0;
		m_step = 0;
	}
	
	// 与单向链表相比,仅有插入、查找和删除操作的实现略微不同
	// 其他函数基本一致,为节省篇幅,其他函数只写声明
	bool move(int i, int step = 1);
	bool next();
	bool end(); // 由于是循环链表,可以无限循环,因此没有结尾,end函数可以省略
	bool set(int i, const T& obj);
	T get(int i) const;
	bool get(int i, T& value) const;
	int length() const;
	void clear();
	
	// 重新实现插入、删除、查找操作
	
	bool insert(const T& obj)
	{
		return insert(m_length, obj);
	}
	
	bool insert(int i, const T& obj)
	{
		bool ret = (0 <= i) && (i < m_length);
		if( ret )
		{
			Node* n = new Node();
			n->data = obj;
			Node* current = &header;
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			// 如果是头插,需要注意分类讨论
			if( current == &header )
			{
				if( current->next == NULL )// 空链表
				{
					current->next = n;
					n->next = n;	// 独自成环
				}
				else
				{
					Node* tail = &header;	// 得到尾结点
					for(int j = 0; j < m_length; ++ j)
					{
						tail = tail->next;
					}
					n->next = current->next;
					current->next = n;
					tail->next = n;	// 首尾相连
				}
			}
			else
			{
				n->next = current->next;
				current->next = n;
			}
			
			++m_length;
		}
		else
		{
			throw(0);
		}
		
		return ret;
	}
	
	bool remove(int i)
	{
		bool ret = (0 <= i) && (i < m_length);
		
		if( ret )
		{
			Node* current = &header;
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			Node* todel = current->next;
			
			// 如果游标正好位于被删除结点位置,则移动游标
			if( m_current == todel && (m_length > 1) )
			{
				m_current = m_current->next;
			}
			
			// 删除头节点同样需要注意分情况
			if( current == &header )
			{
				if( 1 == m_length )
				{
					current->next = NULL;
					m_current = NULL:
				}
				else
				{																
					Node* tail = &header;// 获取尾结点
					for(int j = 0; j < m_length; ++ j)
					{
						tail = tail->next;
					}
					
					current->next = todel->next;
					tail->next = todel->next;// 保持环状
				}
			}
			else
			{
				current->next = todel->next;							
			}
			
			--m_length;	// 先后顺序考虑
			delete todel;	// 异常安全问题
		}
		else
		{
			throw(0);
		}
		
		return ret;
	}
	
	int find(const T& value) const
	{
		int ret = -1;
		Node* current = header.next;
		
		for(int i = 0; i < m_length; ++ i)
		{
			if( current->data == value )
			{
				ret = i;
				break;
			}
			current = current->next;
		}
		
		return ret;
	}
};

#endif

现在就可以用单向循环链表来搞定约瑟夫环。好可怕的游戏!!

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

using namespace std;

// 参数n为参与人数 ,s 为第一个开始的人 , m 为数到的那个倒霉鬼
void josephus(int n, int s, int m)
{
	CircleList<int> cl;
	
	// 人数从 1 开始
	for(int i = 1; i <= n; ++ i)
	{
		cl.insert(i);
	}
	
	cl.move((s-1,m-1);// 链表下标从 0 开始,所以要相应减一
	
	while( cl.length() > 0 )
	{
		cl.next();
		cout << "Suicided person : " << cl.current() << endl;
		cl.remove(cl.find(cl.current()));// 自杀了,就离开这个圈圈
	}
}

int main(int argc, const char* argv[])
{
	josephus(41, 1, 3); // 41个人, 从第一个开始, 数到 3 就自杀
	
	return 0;
}

在这里插入图片描述

在这里插入图片描述

约瑟夫和他的朋友站在16和31的位置那就平安大吉了。不得不感叹,数学不好,真的会s。

一鼓作气搞定双向链表以及双向循环链表,有了单向链表和单向循环链表的基础,实现起来就很轻松了。但需要强调再强调的是前驱和后继指针一定要小心赋值的顺序,指针操作不当很容易就导致整个链表状态出错。
看看图就马上能理解双向链表以及双向循环链表了。

在这里插入图片描述
双向循环链表确实的,有点点复杂,不过办法总比困难多!

在这里插入图片描述

把图放在心中,注意细节,目标代码就很清晰了。双向链表可以复用单向链表中的绝大部分代码,双向循环链表也是可以复用单向循环链表中的绝大部分代码,所以掌握了单向的,实现双向的就轻而易举了。上代码:

#ifndef DUALLINKLIST_H
#define DUALLINKLIST_H

template <typename T>
class DualLinkList
{
protected:
	struct Node
	{
		T data;
		Node* next;
		Node* pre;	// 增加前驱指针
	};

	int m_length;
	int m_step;
	Node* m_current;
	mutable Node header;
public:
	DualLinkList()
	{
		m_length = 0;
		m_step = 0;
		m_current = NULL;
		header.next = NULL;
		header.pre = NULL;
	}
	// 可以复用单向链表的函数就仅仅写声明,具体实现参照上述的单向链表
	bool set(int i, const T& value);
	bool get(int i) const;
	bool get(int i, T& value) const;	
	bool move(int i, int step = 1);
	bool next();
	bool end();
	bool insert(const T& obj);
	int find(const T& value);
	void clear();
	
	bool insert(int i, const T& obj)
	{
		bool ret = (0 <= i) && (i <= m_length);
		
		if( ret )
		{
			Node* n = new Node();
			n->data = obj;
			
			Node* current = &header;	// 找前驱结点
			for(int j = 0; j < i; ++j)
			{
				current = current->next;
			}
			
			Node* next = curret->next;	// 得到后继结点
			
			if( current == &header )	// 头插
			{
				n->pre = NULL;
				n->next = next;
				
				if( next != NULL )	// 如果不是空链表
				{
					next->pre = n;
				}
				
				current->next = n;
			}
			else
			{
				current->next = n;	// 四步操作
				n->pre = current;
				n->next = next;
				
				if( next != NULL )
				{
					next->pre = n;
				}
			}
			++m_length;
		}
		else
		{
			throw(0);
		}
		return ret;
	}
	
	bool remove()
	{
		bool ret = (0 <= i) && (i < m_length);
		
		if( ret )
		{
			Node* current = &header;	
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			Node* todel = current->next;
			Node* next =todel->next;
			
			if( m_current == todel )	// 注意游标位置
			{
				m_current = m_current-next;
			}
			
			if( current == &header )	// 头删
			{
				current->next = next;
				
				if( next !=- NULL )
				{
					next->pre = NULL;
				}
			}
			else
			{
				current->next = next;
				next->pre = current;
			}
			
			--m_length;	// 考虑异常安全
			delete todel;
		}
		else
		{
			throw(0);
		}
	}
	
	bool pre() // 双向链表,提供前向遍历,与后向遍历相呼应
	{
		int i = 0;
		
		while( (i < m_step) && !end() )
		{
			m_current = m_current->pre;
			i++;
		}
		
		return (i == m_step);
	}
	
};

#endif

搞定一个双向链表啦,赶紧试试一下功能正常不。

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

using namespace std;

int main(int argc, const char* argv[])
{
	DualLinkList<int> dl;	
	for(int i = 0; i < 10; ++ i)
	{
		dl.insert(i);
	}
	
	cout << " length = " << dl.length() << endl;
	
	for(dl.move(0)); !dl.end(); dl.next()	// 正向遍历
	{
		cout << dl.current() << " ";
	}
	
	for(dl.move(dl.length()-1); !dl.end(); dl.pre())	// 逆向遍历
	{
		cout << dl.current() << " ";
	}
	
	return 0;
}

在这里插入图片描述

搞完最后一个双向循环链表就可以收工,加油!

#ifndef DUALCIRCLELIST_H
#define DUALCIRCLELIST_H

template <typename T>
class DualCircleList
{
protected:
	strcut Node
	{
		T data;
		Node* next;
		Node* pre;
	}
	int m_length;
	int m_step;
	Node* m_current;
	mutable Node header;
public:
	DualCircleList()
	{
		m_length = 0;
		m_step = 0;
		m_current = NULL;
		header.next = NULL;
		header.pre = NULL;
	}
	
	// 除了插入和删除操作与众不同,其他完全可以复用单向链表和单向循环链表
	// 以及双向链表中的相关功能函数
	
	bool insert(const T& obj)
	{
		return insert(m_length, obj);
	}
	
	bool insert(int i, const T& obj)
	{
		bool ret = (0 <= i) && (i <= m_length);
		
		if( ret )
		{
			Node* n = new Node();
			n->data = obj;
			
			Node* current = &header;
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			Node* next = current->next;
			
			if( current == &header )	// 头插
			{
				if( current->next == NULL )	// 空链表,独自成环
				{
					current->next = n;
					n->next = n;
					n->pre = n;
				}
				else
				{
					Node* tail = &header;	// 获取尾结点
					for(int j = 0; j < m_lengh; ++ j)
					{
						tail = tail->next;
					} 
					
					current->next = n;	// 连接结点
					n->next = next;
					n->pre = tail;
					next->pre = n;
					tail->next = n;																				
				}
			}
			else
			{
				current->next = n;
				n->next = next;
				n->pre = current;
				next->pre = n;
			}
			
			++m_length;
		} 
		else
		{
			throw(0);
		}
		
		return ret;
	}
	
	bool remove(int i)
	{
		bool ret = (0 <= i) && (i < m_length);
		
		if( ret )
		{
			Node* current = &header;	// 得到前驱结点
			for(int j = 0; j < i; ++ j)
			{
				current = current->next;
			}
			
			Node* todel = current->next;
			Node* next = todel->next;
			
			if( m_current == todel && m_length > 1 )	// 注意游标的位置是否与被删除的结点相同
			{
				m_current = m_current->next;
			}
			
			if( current == &header )	// 头删
			{
				if( m_length == 1 )	// 一个元素
				{
					m_current = NULL;
					current->next = NULL;
				}
				else
				{
					Node* tail = &header;	// 得到尾结点
					for(int j = 0; j < m_length; ++ j)
					{
						tail = tail->next;
					}
					
					current->next = next;
					tail->next = next;
					
					if( tail == next ) // 两个元素的情况,
					{
						tail->pre = next; 
					}
					else	// 还有多个元素的情况
					{
						next->pre = tail;
					}
				}
			}
			else
			{
				current->next = next;
				next->pre = current;
			}
			
			--m_length;
			delete todel;
		}
		else
		{
			throw(0);
		}
		
		return ret;
	}
};

#endif

测试代码可以直接修改单向循环链表解决约瑟夫环的程序。

猜你喜欢

转载自blog.csdn.net/Dream136/article/details/106783629