数据结构与算法-列表(双向链表)设计及其排序算法

0 概述

本文主要涵盖列表(双向链表)的设计及其排序算法的总结。列表是一种典型的动态存储结构。其中的数据,分散为一系列称作节点(node)的单位,节点之间通过指针相互索引和访问。为了引入新节点或删除原有节点,只需在局部调整少量相关节点之间的指针。这就意味着,采用动态存储策略,可以大大降低动态操作的成本。

1 列表(双向链表)设计

1.1 结点的设计
列表是由一个个结点链接而成,其基本结构便是节点(下文称节点为Node)。Node的良好设计可以减少列表的设计难度。根据列表的特性,一个Node一般包括数据、指向前驱的指针和指向后继的指针。很显然我们需要将其封装入一个结构体(C++增强了结构体的功能,可以作为类使用)或类内,为了优化列表的设计、简化Node的使用,
1)我们可以设计Node的构造函数并在构造函数内为其指定前驱和后继;
2)除此以外,我们还可以定义插入当前节点之前和之后的插入函数,这可以简化列表的Node的插入操作。
Node的设计函数如下所示:

//列表结点的设计
struct MyListNode
{
	//数据定义
	T m_data;
	//指向前驱的指针
	MyListNode<T>* m_pred;
	//指向后继的指针
	MyListNode<T>* m_succ;
	
	//构造函数
	//默认构造函数,前驱和后继为nullptr
	MyListNode() { m_pred = nullptr; m_succ = nullptr; }
	//可以指向前驱和后继的构造函数
	MyListNode(T data, MyListNode<T>* pred=nullptr, MyListNode<T>* succ= nullptr)
		:m_data(data),m_pred(pred),m_succ(succ){}

	//操作接口
	//由于创建新节点时已经创建好一条链接关系
	//若作为当前节点的前驱插入,则注意:1谁作为当前节点的前驱 2谁的后继是当前节点
	//若作为当前节点的后继插入,则注意:1当前节点是谁的前驱 2当前节点的后继是谁

	//作为当前节点前驱插入
	MyListNode<T>* InsertAsPred(const T& data)
	{         
		//创建新节点,以当前节点的前驱作为前驱,以当前节点作为后继 则  pred<-- new node -->this
		MyListNode<T>* p_node = new MyListNode<T>(data, m_pred, this);
		
		//由于构造新结点时已经为新结点指定了前驱和后继
		//故这里只需将当前节点的前驱后继设为新节点,当前节点的前驱设为新节点
		//  pred--> new node <-- this
		
		//将当前节点的后继设为新节点
		m_pred->m_succ = p_node;

		//将当前节点的前驱设为新节点
		m_pred = p_node;
		
		return p_node;
	}

	//作为当前节点后继插入
	MyListNode<T>* InsertAsSucc(const T& data)
	{
		//插入原理 与InsertAsPred 类似
		//创建新节点,以当前节点作为前驱,当前节点的后继作为后继  this <-- new node --> succ
		MyListNode<T>* p_node = new MyListNode<T>(data, this, m_succ);
		
		// 由于构造新结点时已经为新结点指定了前驱和后继
		//故这里只需将当前节点的前驱后继设为新节点,当前节点的前驱设为新节点
		// this <-- new node <-- succ
		
		//当前节点的后继指向新节点
		m_succ = p_node;

		//当前节点的后继指向新节点
		m_succ->m_pred = p_node;

		return p_node;
	}
};

1.2 列表的设计
列表是在节点的基础上设计的,良好的节点设计可以大大简化列表的设计。
列表的设计包括一个私有的头节点(header)和尾节点(tailer),二者对外不可见,仅用于对数据的管理和跟踪,不用于数据的存储。
列表的第1个元素为header的后继,最后1个元素为tailer的前驱。若列表内没有元素则header的后继为tailer,tailer的前驱为header。
综上,列表的结构如下:
在这里插入图片描述

2 列表的搜索算法

由于列表不可随机存取,故列表只能根据提供的起始节点进行逐一对比查找,故其查找的复杂度O(n).
但是根据其查找的用途不同,我们根据列表是否有序进行不同的查找,返回不同的结果。
1)对于无序列表,若找不到需要的数据,则直接返回nullptr即可;
2)但是对于有序列表的查找,我们一般需要使用其返回结果进行插入、排序之类的操作,所以对其返回结果有特别的要求。
2.1 无序列表的查找
找到直接返回对应节点的地址,找不到直接返回nullptr.

//对无序列表进行查找,若没有找到则直接返回nullptr
//该查找向前查找,向后查找与此类似
// data   :需要查找的数据
// num    :向前查找的数据数目
// p_node :查找的起始点
//若查找整个序列 则为 Find(data,myList.Size(),myList.Last());
template<typename T>
MyListNode<T>* MyList<T>::Find(T data,int num, MyListNode<T>* p_node)
{
	//依次向前查找逐一比对
	while ((num--) && ((p_node =p_node->m_pred)!= m_header))
	{
		if (data == p_node->m_data)
		{
			return p_node;
		}
	}

	//若找不到则返回nullptr
	return nullptr;
}

2.2 有序列表的查找
对于有序列表的查找,我们一般需要使用其查找结果。假设有序列表升序排列(也就是从小到大排列,也可能有重复数据),如果我们需要将一数据A插入到当前列表List且仍保持List有序,那么
1)如果能在列表中找到该数据A,则我们希望其插入到重复数据的最后一个;
2)如果不能找到该数据A,则我们希望返回小于该数据的第一个数据B的位置Addr_B,那么我们可以将数据A插入数据B的后面而保持List有序。
为了满足上述要求,则有序列表的查找算法的返回值应为上述情况对应的值,查找算法如下设计:

//适合有序列表的查找方法,返回小于当前元素的第一个位置
//或者返回等于该元素的重复元素的最后一个位置
//如果没有找到小于或等于当前元素的元素,则返回左边界的前驱.有可能是m_header
//该查找向前查找,向后查找与此类似
// data   :需要查找的数据
// num    :向前查找的数据数目
// p_node :查找的起始点
//若查找整个序列 则为 Find(data,myList.Size(),myList.Last());
template<typename T>
MyListNode<T>* MyList<T>::Search(T data, int num, MyListNode<T>* p_node)
{
	while (num-- >= 0)
	{
		if ((p_node = p_node->m_pred)->m_data <= data)
			break;
		//找到小于等于指定元素的元素则终止查找,返回小于当前元素的第一个位置
		//或者返回重复元素的最后一个位置
	}

	return p_node;
}

3 列表的排序算法

列表的排序算法主要包括插入排序、选择排序和归并排序
3.1 插入排序
插入排序的算法的思路为:
1)设当前节点p_node之前的数据为有序的(设为front_list),之后为无序的;
2)利用有序查找Search函数,找到当前节点p_node在front_list中的位置,将当前节点插入到front_list;
3)p_node移动到下一节点,删除已经插入到front_list的节点。
示意图如下
在这里插入图片描述
实现如下:

/插入排序实现
//p_node :开始排序的起点
//num     :要排序的节点数目
//example:对全列表排序InsertionSort(myList.First(),myList.Size();
template<typename T>
void MyList<T>::InsertionSort(MyListNode<T>* p_node,int num)
{
	for (int i = 0; i < num; i++)
	{
		//查找当前节点p_node在p_node之前的(有序)数据中应在的位置insert_index
		MyListNode<T*> insert_index = Search(p_node->m_data, i, p_node);
		//将当前节点插入 insert_index
		InsertAsAfter(insert_index, p_node->m_data);
		//移至下一节点
		p_node = p_node->m_succ;
		//删除已插入有序数据的原节点
		Remove(p_node->m_pred);
	}
}

3.2 选择排序
选择排序的基本思路为:
1)将列表分为两部分,前半部分的数据为无序的(设为front_list),后半部分的数据为有序的(设为rear_list);
2) 查找front_list中的的最大值max_node并将其从front_list中移除;
3)将max_node插入到front_list的最后端,也是有序列表第一位之前;
4)front_list减少1节点,rear_list增加1节点,再循环2-4步骤;
示意图如下:

在这里插入图片描述
代码实现如下:

/选择排序
//p_node  :排序的起点
//num     :要排序的节点的数目
//示例    :对全list排序  SeletcionSort(myList.First(),myList.Size());
template<typename T>
void MyList<T>::SelectionSort(MyListNode<T>* p_node, int num)
{
	//定义有序列表的第一个元素位置
	MyListNode<T>* tailer = p_node;

	//定义无序列表的第一个位置的前驱
	MyListNode<T>* header = p_node->m_pred;
	
	//显然,选择排序需要从后往前排序
	//该步骤循环往后,定义有序列表的第一个元素位置
	//若当前有序列表为空,则为待排序列表最后一个元素的后继,可能为m_tailer
	for (int i = 0; i < num; i++)
		tailer = tailer->m_succ;
	
	for (int i = num; i >1; i--)
	{
		//寻找无序列表的最大值
		MyListNode<T>* max_node = SelectMax(header->m_succ, i);
		//移除无序列表中的最大值max_node,并将最大值插入到无序列表的最后一位,也是有序列表第一位之前
		//相当于交换二者的值
		InsertAsBefore(tailer,Remove(max_node));

		//有序列表的第一位元素向前移动一位
		//有序序列增加一个节点
		tailer = tailer->m_pred;
		
		//for循环中的i-- 表示无序序列减少1个节点,SelectMax查找的节点数目减少1个
	}
}

3.3归并排序
归并排序的基本思路为:
1)分而治之,将无序列表逐次分为前列表(front_list)和后列表(rear_list),直至两个列表只有一个元素;
2)将front_list与rear_list按照顺序合并,由于列表分割是从大到小,故合并后仍为列表的一部分设为half_list,然后与对应的另一半other_half_list进行有序合并,循环往复直至归并完成,排序也相应完成。
示意图如下所示:
在这里插入图片描述代码实现如下所示:


//p_node   :需要归并的列表P(当前列表)的起始点
//p_num    :需要归并的列表P(当前列表)的节点数目
//src_list :需要归并的列表Q
//q_node   :需要归并的列表Q的起始点
//q_num    :需要归并的列表Q的节点数目
//该函数是将q_node开始的q_num个节点归并到p_node开始的列表内
template<typename T>
void MyList<T>::Merge(MyListNode<T>* &p_node, int p_num, MyList<T> &src_list,MyListNode<T>* q_node, int q_num)
{
	//保存当前列表P的起始点p_node的前驱作为归并后列表的前驱
	//这一点很重要,因为后边列表的变更可能修改第一项从而导致无法寻项
	MyListNode<T>* header = p_node->m_pred;
	
	//下列程序中将p_node作为归并列表(MergeList)中最后一个元素的后继来使用
	//q_num==0 表示Q列表归并完成
	while (0<q_num)
	{
		//如果P列表未归并完成且p_node->m_data <= q_node->m_data则p_node插入到归并列表MergeList
		if ((0 < p_num) && (p_node->m_data <= q_node->m_data))
		{
			//p_node被归并后移动到下一节点
			p_node = p_node->m_succ;
			//若干p_node==q_node,则P列表已归并完成
			if(q_node==p_node)
				break;
			//P列表虚归并的节点数目减1
			p_num--;
		}
		else
		{
			//若P列表归并完成或者p_node->m_data > q_node->m_data则q_node插入到汇报列表MergeList
			InsertAsBefore(p_node, q_node->m_data);
			//q_node被归并后移动到下一节点
			q_node = q_node->m_succ;
			//移除已经归并后的q_node
			src_list.Remove(q_node->m_pred);

			//Q列表需归并的节点数目减1
			q_num--;
		}
	}
	p_node = header->m_succ;
}


//归并排序的实现
//p_node  :要排序列表的起始节点
//num     :要排序节点的数目
template<typename T>
void MyList<T>::MergeSort(MyListNode<T>* &p_node, int num)
{
	if (num < 2)
		return;
	//求出列表的中点
	int mid = num >> 1;           

	//求出后半列表的起始节点
	MyListNode<T>* q_node = p_node;
	for (int i = 0; i < mid; i++)
	{
		q_node = q_node->m_succ;
	}
	MergeSort(p_node, mid);                       //对前半部分列表进行递归划分
	MergeSort(q_node, num - mid);                 //对后半部分列表进行递归划分
	Merge(p_node, mid, *this, q_node, num - mid); //对划分好的前后列表进行归并,依次迭代直至归并完成,排序也完成
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值