Bootstrap

大二下开始学数据结构与算法--04,合并两个有序单向链表

自习所完成的任务

  1. 完成合并俩个有序单项链表,并测试。
  2. 实现判断单链表是否存在环,以及环节点位置。

任务

明日任务

  1. 学习教程至P22。

周六任务

  1. 复现单链表。

感悟

  1. 今天去看招聘会,个人感觉会更倾向独自或者团队合作所完成的项目或者实践经历,然后才是你的一些奖项。然后就是一些必要的生活技能,如会开车之类。
  2. 今天下午的的学习时间从四点开始,到五点半,然后从八点开始到九点,时间的利用效率确实不高,这也是为什么,我没有进行单链表进行复现的原因,又开始时间换进度了,笑死。
  3. 明天会上体育课,重置一下状态然后后两天我上自习的教教室会被封楼,我尽量找个好地方进行学习。
#include<iostream>
#include<stdlib.h>
#include<time.h>
#include<string>
using namespace std;


//单链表,设置一个头节点,以方便后续的数据的插入;
//可以将链表的每个节点看成一个单元,每个单元都有着当前的数据,以及指向下一个节点的地址(先一个节点的数据类型是定义好的,所以就要用自主定义的数据类型)

//节点类型
struct Node
{
	Node(int date=0):date_(date),next_(nullptr){}
	int date_;
	Node *next_;//为什么可以这么做?如果是这样的话,这个指针的大小是多少?以Node这个为数据类型的变量的大小是多少?(我记得之前在哪看过,好像是只包含构造函数的)
	/*我的理解是,无论是结构体还是类,都是对一类事情的抽象共性的集合。这样的定义一个Node类型的指针就很正常了。*/
};

class Clink
{
	friend void ReverseLink(Clink&link);
	friend bool GetLastKNode(Clink&link,int k,int &KVal);
	friend void Showone(Clink &link,int num);
	friend void MergeLink(Clink &link1,Clink &link2);
	//friend bool IsLinkHaveCircle(Clink &link,int &val);
public:
	Clink()
	{
		//给head_初始化指向头节点
		head_= new Node();//会自动开启一个类型为Node的空间,还会自动初始化head_这个节点//为什么要new出来?
		/*首先head_是一个Node类型的指针,这个指针只记录地址,并不包含其他内容,因此,要实例化一个Node类型的变量,这里使用的无参构造,通过new返回地址,同时,也要注意new开辟在堆区的内存的释放*/
	}
	~Clink()
	{
		//结点的释放
		/*其实这个东西就是"过河拆桥",你需要先找好退路,才能大胆进行断后路,很多思想都能在现实中找到例子,生活经历的多了,也许对编程也会有帮助,同时,也不要将编程想的太高大上,让变成生活化,也许是算法进步的一种途径*/
		Node*p=head_;/*从头部向尾部进行释放,通过直接转移头节点,从而减少变量的生成,是代码变得精简,这一点需要学习;然后就是关于什么时候用head_->next,head_这一点你需要仔细琢磨一下;do while ,while仔细思考一下*/
		while(p!=nullptr)
		{
			head_=head_->next_;
			delete p;
			p=head_;
		}
		head_=nullptr;//为什么一定要将这个定义成空指针?其他的不需要吗?
	}
private:
	Node*head_;//指向链表的头节点的指针
public:
	//链表的尾插法  时间复杂度O(n)./*时间主要浪费在找尾节点上,假若我给出一个尾节点,那么时间复杂度就会降低为O(1)*/
	void insertTail(int val)
	{
		//先找到当前链表的末尾节点。这个尾节点是通过一个新定义的指针进行寻找的。
		Node*p=head_;//算不算拷贝构造函数?如果算的话,这不就是浅拷贝吗?不会出现错误吗?每次尾插都会建立一个Node类型的指针,可以释放吗?/*不算拷贝构造函数,他只是初始化了一个Node类型的指针,指针的内容为head_的地址,这个属于区域变量,会在结束后,自动释放空间。拷贝构造实质上是通过一个类或者结构体来实例化另一个结构体或者类。*/
		while(p->next_!=nullptr)//这里为什么要用p->next_?/*尾插法的核心就是找到尾节点,后者说是当前节点的属性指针指向空节点,因此,用这个方法*/
		{
			p=p->next_;
		}

		//生成新的节点
		Node*node=new Node(val);/*这个肯定是要释放的*/
		//将新的节点的地址放在原尾节点的后面
		p->next_=node;
		//关于这里各种元素的声明,实例,和调用,我有点不清楚原理。这个好像就是一个框架,之后的东西就是套进去写的。将字体缩小之后,从总的角度来看,变量的所属有点不清晰,比如,Node*p=head_这句话,我就感觉有点没头没尾的?
	}
	//链表的头插法 时间复杂度O(1)
	void insertHead(int val)
	{
		//这一段尾插法的后半段很像,就是通过建立一个新的Node类型指针进行传递
		Node *node=new Node(val);//这里肯定是要释放的,通过谁的析构函数进行释放?
		node->next_=head_->next_;
		head_->next_=node;
	}

	//链表节点的删除(删除值为val的节点)
	void Remove(int val)//AI写的,感觉更好,(看了几遍后,感觉有点麻烦,在存疑)因为我不熟悉do...while,和while,已经修改了一下,个人感觉还挺好。
	{
		Node *node = head_;
		Node *p = head_->next_;
		while (p != nullptr)//有点不明白,如何通过值的转换,来达到改变节点指针指向的操作?/*通过node,p这两个指针来实现,node,p记录的是节点的地址,通过对地址的解引用进而对节点属性进行操作,达到改变节点指针指向的目的*/
		{
			if (p->date_ == val)
			{
				node->next_ = p->next_;
				delete p;/*因为跳过之后,那个节点仍然占据空间,所以,要删除掉*/
				return;/*只删除最先遇到符合条件的话,直接用return就行了*/
				//p = node->next_; /*更新 p 指针,继续遍历,达到持续释放的效果*/
				
			}
			else
			{
				node = p;
				p=p->next_;
			}
		}
	}
	
	//删除所有符合条件的值
	void RemaveAll(int val)
	{
		Node*p=head_->next_;
		Node*node=head_;
		while(p!=nullptr)
		{
			if(p->date_==val)
			{
				node->next_=p->next_;
				delete p;
				p=node->next_;
			}
			else
			{
				node=p;
				p=p->next_;
			}
		}

	}
	
	//搜索(判断是否存在给定值),时间复杂度是O(n),线性搜索
	bool Find(int val)
	{
		Node * p=head_->next_;/*找有效元素,不需要从头节点开始找,从第一个有效节点开始就行;也可以在头节点中添加节点的个数*/
		while(p!=nullptr)
		{
			if(p->date_==val)
			{
				return 1;
			}
			else
			{
				p=p->next_;
			}
		}
		return 0;
	}

	//链表打印;
	void Show()
	{
		Node *node=head_->next_;
		while(node!=nullptr)
		{
			cout<<node->date_<<" ";
			node=node->next_;
		}
	}
	//链表打印第k个,通过成员函数实现,还能优化结构;测试一下极端值;倒数第0个?/*倒数第0个感觉没有什么实际意义,提示过后,直接退出程序就行*/
	/*其实我感觉没必要通过成员函数实现,在我的理解里,成员函数是抽象出来的公用功能,像打印第k个元素这样的细节型,通过全局函数进行处理最好*/

	//展示正数第num个元素,成员函数
	void Showone(int num)
	{
		int count=1;
		int val=0;
		Node *p=head_->next_;
		if(num<=0)
		{
			cout<<"节点不存在";
		}
		if(p==nullptr)//改为先判断,后输出,这样比较好
		{
			cout<<"空链表"<<endl;
			return;
		}
		while(1)
		{
			if(p==nullptr)/*到达尾节点,终止函数*/
			{
				return;
			}
			val=p->date_;
			if(count==num)
			{
				cout<<val;
				return ;
			}
			count++;
			p=p->next_;
		}
	}
};

//展示正数第num个元素,全局函数
	void Showone(Clink &link,int num)
	{
		int count=1;
		int val=0;
		Node *p=link.head_->next_;
		if(num<=0)
		{
			cout<<"节点不存在";
		}
		if(p==nullptr)//改为先判断,后输出,这样比较好
		{
			cout<<"空链表"<<endl;
			return;
		}
		while(1)
		{
			if(p==nullptr)/*到达尾节点,终止函数*/
			{
				return;
			}
			val=p->date_;
			if(count==num)
			{
				cout<<val;
				return ;
			}
			count++;
			p=p->next_;
		}
	}


//链表元素逆序,关注复杂度(时间/空间)
/*函数作为友元,来访问类中的私有属性;引用传递;头插法变形以达到逆序目的*/
void ReverseLink(Clink & link)//这里一开始写的是Node *head,在main中对应函数写入(link.head_)会报错;后来修改成的这,引用传递,直接传入参数就行,能够改变参数具体元素的值;那再改成Clink * link,main函数中写&link行不?
{
	Node*p=link.head_->next_;
	link.head_->next_=nullptr;/*作用是,重置初始化头节点,这里还挺关键的*/
	if(p==nullptr)
	{
		return;
	}
	else
	{
		while(p!=nullptr)
		{
			Node*q=p->next_;
			p->next_=link.head_->next_;/*类似于头插法,若为第一次,这p->next_为nullptr,其余情况为上一个节点的地址,这里还挺精巧的*/
			link.head_->next_=p;
			p=q;
		}
	}
}

//通过双指针求倒数第k个元素
bool GetLastKNode(Clink&link,int k,int &KVal)
{
	Node *p=link.head_->next_;
	Node *q=link.head_->next_;
	if(k<1)
	{
		cout<<"k值过小";
		return false;
	}
	for(int i=0;i<k-1;i++)
	{
		p=p->next_;
		if(p==nullptr)
		{
			cout<<"k值超过节点数";
			return false;
		}
	}
	while(p->next_!=nullptr)
	{	
		q=q->next_;
		p=p->next_;
	}
	KVal=q->date_;
	return true;
}

//合并两个有序单链表,成为一个,小->大;如果从大->到小呢?
void MergeLink(Clink &link1,Clink &link2)
{
	//构建双指针
	Node*p=link1.head_->next_;
	Node*q=link2.head_->next_;
	Node*last=link1.head_;

	while(p!=nullptr && q!=nullptr)
	{
		if(p->date_<q->date_)//为什么last的改变能影响到head_->next_的指向?/*->是解码得到地址所对应的对象,然后按要求操作对象*/
		{
			last->next_=q;
			q=q->next_;
		}
		else//稳定性怎么样?
		{
			last->next_=p;
			p=p->next_;
		}
		 last = last->next_;
		
	}
	if(p==nullptr)
	{
		last->next_=q;
	}
	else
	{
		last->next_=p;
	}

	link2.head_->next_=nullptr;
}

//判断单链表是否存在环,存在环,返回节点
bool IsLinkHaveCircle(Node*head,int &val)
{
	//定义快慢指针
	Node *fast=head;
	Node *slow=head;

	while(fast!=nullptr&&fast->next_!=nullptr)
	{
		fast=fast->next_->next_;
		slow=slow->next_;

		if(slow==fast)
		{
			int count=0;
			fast=head;
			while(fast!=slow)
			{
				fast=fast->next_;
				slow=slow->next_;
				count++;
			}
			val=count;
			return true;
		}
	}

}

//构造成环链表//到这确实累了,明天看一下,如何生成一个环状单向链表,然后将函数接口换成链表指针
int main()
{
	Node head;
	Node n1(25),n2(67),n3(32),n4(18);
	head.next_=&n1;
	n1.next_=&n2;
	n2.next_=&n3;
	n3.next_=&n4;
	n4.next_=&n2;

	int val;
	if(IsLinkHaveCircle(&head,val))
	{
		cout<<"存在环,节点数为"<<val;
	}
	else
	{
		cout<<"否";
	}
	cout<<endl;
	system("pause");
	return 0;
}

//合并两条有序单向链表,小->大
#if 0
int main()
{
	//节点数据
	int arr[8]={25,37,52,108};
	int brr[7]={13,23,40,109};

	//初始化链表
	Clink link1,link2;

	//给链表节点赋值
	for(int i=3;i>=0;i--)
	{
		link1.insertTail(arr[i]);
		link2.insertTail(brr[i]);
	}
	//link1.insertTail(arr[7]);

	//测试点
	link1.Show();
	cout<<endl;
	link2.Show();
	cout<<endl;

	//函数测试
	MergeLink(link1,link2);
	link1.Show();
	cout<<endl;
	link2.Show();
	cout<<endl;

	/*看题目要求,如果要求link2打印为空,就将头指针之置为空,这样,link2的节点就只被link1使用,在函数析构时,link1的析构函数直接全部释放节点;如果不要求,这link2正常输出节点,因为last只是将节点串了起来,并没有改变link2原有的节点顺序,地址之间相互联系就会构成一个环路,这样会导致同一节点被二次释放,导致出错*/
	system("pause");
	return 0;
}
#endif

;