C++/C语言--算法-链表学习 单链表/双向链表 循环链表 链表的插入 删除 冒泡排序 快速排序 合并两个有序列表 查找中间结点 查找倒数第k个结点

文章目录

  • *数据结构-链表*
    • 一,单向(无循环)链表
      • 1.三种插入元素的方法:
        • ①头插法:
        • ②尾插法:
        • ③中间插入法(在目标值之后插入一个结点):
      • 2.删除元素(不遍历链表):
    • 二.双向(无循环)链表
      • 1.三种插入方法
        • ①头插法:
        • ②尾插法:(一样的啦)
        • ③中间插入(主要就讲一下关键部分啦):
      • 2.双向列表的浅浅删除
    • 三.单向/双向循环链表
    • 四.对链表的一些简单操作(以单链表为例)
      • 1.链表的逆置/反转
        • ①递归法dfs(参数是两个,一个是子结点next,一个是父节点)
        • ②迭代法
        • ③就地逆置法
      • 2.冒泡排序
      • 3.快速排序
      • 4.合并两个有序列表
      • 5.查找中间结点
      • 6.打印链表
      • 7.删除链表
      • 8.查找倒数第k个结点
      • 9.判断单链表是否带环
      • 10.求出带环链表的长度
      • 11.求出带环链表环的进口
      • 12.
    • 五.实战演练
      • 约瑟夫环

数据结构-链表

PS:(写在博客前的话)
关于我自己对链表的理解,感觉就是一种类似于数组,但是每个单位储存的值的类型可以多种多样而不是单一的,并且插入和删除的操作会比数组快的多,但是相对于数组来说,链表的查询速度相对较慢,最差可以达到O(n)复杂度,而数组查询元素只需要O(1)时间,链表的各个结点我称之为储存单元,每个储存单元分为了两部分,我姑且称之为值域和指针域,值域即储存该单元储存的各种值(可以多个),然后指针域则是指向另一个或指向另外多个结点,从而达到前进/后退的目的。

以下均为无头链表

一,单向(无循环)链表

在这里插入图片描述
由图可知,这种链表是不能从后往前的
首先,我们要初始化一个链表
step 1:创建出链表节点的类

struct Node
{
//值域
	int val;
//指针域
	struct Node* next;
};

step 2:创建出链表的头指针
为了方便我们做这样的一个定义

typedef Node* LinkNode;

接下来初始化一个头指针(用来分辨是哪个链表的,比如head1是第一个链表
head2是第二链表…)

LinkNode head = NULL;  //初始化一个链表

现在,我们已经创建好了一个链表,现在要怎么办?当然是往链表里造东西啊!
step 3:插入元素
这里提供三种方法

1.三种插入元素的方法:

①头插法:
顾名思义,每次插入都在头部的位置插入元素,比如,依次插入1,2,3,4,5,6;打印出来的链表应该是6,5,4,3,2,1
void insert_head(LinkNode* head, int val)//头插法

对于形参列表的解释:LinkNode* 实际上是一个二级指针,因为LinkNode是Node的意思,为何我们要传二级指针而不是直接LinkNode head?因为我们可能会改变头指针的值(地址),如果我们直接传LinkNode 传的是形参,即我们此时不能在函数内部直接改变head,这和传a进函数中,在函数内部改变其形参的值不会直接改变a是一个道理,只有传a的地址进去,然后通过地址改变a才能达到直接改变a的效果,所以用LinkNode 。第二个参数,即你要插入的值


然后呢,我们要先初始化一个new_code用来插入

LinkNode new_node = (LinkNode)malloc(sizeof(Node));
if (new_node == NULL)//防止开辟失败的问题
	return;
new_node->val = val;

接下来要干嘛,你仔细思考,如果head(即我们初始化的链表的地址)==NULL,即此时链表为空,怎么办,当然是直接把head = new_node啊

if (*head == NULL)//如果链表为空呢
{
	*head = new_node;
	new_node->next = NULL;
	return;
}

OK,现在就是讨论不是空的情况了:
也就是说,现在的head是有值的对不对,我们先用一个临时变量把他存下来(或者先让new_node的next指向head,再改变head),然后把head直接变为现在的new_node,再把new_node的next指向刚才的*head就行了,非常的简单

	new_node->next = *head;
	(*head) = new_node;

就此,我们就做好了一个insert_head函数,全部如下:

void insert_head(LinkNode* head, int val)//头插法
{
	LinkNode new_node = (LinkNode)malloc(sizeof(Node));
	if (new_node == NULL)
		return;
	new_node->val = val;
	if (*head == NULL)
	{
		*head = new_node;
		new_node->next = NULL;
		return;
	}
	new_node->next = *head;
	(*head) = new_node;
}

②尾插法:

方法与上一个几乎一样,唯一不一样的是我们要插在尾部,即插在现在最后一个元素的next处,那我们就要先到这个地方,用一个循环,找NULL

LinkNode tmp = *head;//为何不直接操作*head?如果直接操作你的头指针人呢?
while (tmp->next !=NULL)
{
	tmp = tmp->next;
}


当tmp->next==NULL时,也就是现在已经到尾部了,可以插入了

tmp->next = new_node;
new_node->next = NULL;

全部代码如下

void insert_back(LinkNode* head, int val) //插入方法之尾插法(单链表)
{
	LinkNode new_node = (LinkNode)malloc(sizeof(Node));
	if (new_node == NULL)
		return;
	new_node->val = val;

	if (*head == NULL)//如果链表为空呢
	{
		*head = new_node;
		new_node->next = NULL;
		return;
	}
	LinkNode tmp = *head;
	while (tmp->next !=NULL)
	{
		tmp = tmp->next;
	}
	tmp->next = new_node;
	new_node->next = NULL;
}


③中间插入法(在目标值之后插入一个结点):

思想和尾插法几乎一模一样,只不过把tmp->next!=NULL换成了tmp->val!=目标值罢了。

LinkNode tmp = *head;
while (tmp->next != NULL && tmp->val != target)
{
	tmp = tmp->next;
}

为何要并且一下tmp->next !=NULL?,因为,如果你要查找的目标值不存在或者呢?一直找下去然后解引用*NULL?对吧,所以还是有必要并且一下的


接着马上要判断一下,是因为到最后了停止循环的呢,还是val与target相等停下来的呢?如果是到最后了停止的(还要判断一下最后这个结点的val是不是和target相等,如果相等即正常算就行了,现在讨论target不存在于链表中的情况),你可以选择默认此时在尾部插,也可以选择直接返回,我这里选择前者

//判断一下现在是不是在尾部
if (tmp->next == NULL)
{
	tmp->next = new_node;
	new_node->next = NULL;
}
else//不在尾部
{
	LinkNode tmp2 = tmp->next;
	tmp->next = new_node;
	new_node->next = tmp2;
}

解释一下不在尾部时候的操作,先用一个临时的指针保存一下当前位置的下一个结点的地址,然后将当前结点的下一个位置指向new_node,将new_node指向原来的当前位置的下一个结点(即tmp2)(现在的当前位置的下一个结点时new_node了)即可。


整体的代码:

void insert_mid(LinkNode* head, int target, int val)//中间插法
{
	LinkNode new_node = (LinkNode)malloc(sizeof(Node));
	if (new_node == NULL)
		return;
	new_node->val = val;
	if (*head == NULL)
	{
		*head = new_node;
		new_node->next = NULL;
		return;
	}
	LinkNode tmp = *head;
	while (tmp->next != NULL && tmp->val != target)
	{
		tmp = tmp->next;
	}
	//判断一下现在是不是在尾部
	if (tmp->next == NULL)
	{
		tmp->next = new_node;
		new_node->next = NULL;
	}
	else//不在尾部
	{
		LinkNode tmp2 = tmp->next;
		tmp->next = new_node;
		new_node->next = tmp2;
	}
}

至此我们就完成了插入元素的全部操作


step 4:删除元素

2.删除元素(不遍历链表):

专门写个头删和尾删没有太大必要,所以我们直接讨论一般情况
首先要考虑一下特殊情况: 链表已经为空了,这个时候直接退出就好,不然的话,我们要像中间插入的方法先循环到对应val值的结点处,再对其操作

void DeleteNode(LinkNode* head,int target)
{
    if(*head==NULL)
    {
        cout << "The List is empty!" <<endl;
        return;
    }
    LinkNode tmp = *head;
    while(tmp->next!=NULL&&tmp->val!=target)
    {
        tmp=tmp->next;
    }
    if(tmp->next==NULL&&tmp->val!=target)//即中间插入法一样的原理,如果到最后
    //还没找到对应的target的话,直接返回就好
    {
        cout << "There is not a node that match the target"<<endl;
        return;
    }

(注意,这里的循环是获得你要删除的结点,如果题目直接给你结点的地址了,就省去前面所有的部分,相当于我们这是个完整的程序,给自己玩的,如果题目直接说要给你这个地址的话,那就直接操作我后面要说的东西就好了)


现在考虑,找到了又应该怎么操作呢,我们要删除一个结点,必然要对其前面和后面的结点都操作,获得后继很简单,->next就行,但是要获得前驱,要循环才能获得,所以我们直接把这个点当成前驱,后面那个点当要删除的点,后面的后面那个点当后继,要怎么做才能这样呢?把当前结点的后继的值赋值给当前结点即可。

	LinkNode tmp2 = tmp->next;
	tmp->val = tmp->next->val;//把后继的值覆盖现在的值(相当于删除了当前结点)
	tmp->next = tmp->next->next;//当前结点的下一个结点设置为之前结点的下下个结点
	free(tmp2);//再把tmp2释放掉就行
	tmp2=NULL;

在这里插入图片描述


整体代码如下

void DeleteNode(LinkNode* head, int target)
{
	if (*head == NULL)
	{
		cout << "The List is empty!" << endl;
		return;
	}
	LinkNode tmp = *head;
	while (tmp->next != NULL && tmp->val != target)
	{
		tmp = tmp->next;
	}
	if (tmp->next == NULL && tmp->val != target)//即中间插入法一样的原理,如果到最后
		//还没找到对应的target的话,直接返回就好
	{
		cout << "There is not a node that match the target" << endl;
		return;
	}
	else
	{
		LinkNode tmp2 = tmp->next;
		tmp->val = tmp->next->val;//把后继的值覆盖现在的值(相当于删除了当前结点)
		tmp->next = tmp->next->next;//当前结点的下一个结点设置为之前结点的下下个结点
		free(tmp2);//再把tmp2释放掉就行
		tmp2 = NULL;
	}
}

至此,单链表的所有操作就完结啦!


二.双向(无循环)链表

所有的操作和单向一模一样,只不过多了个前驱而已

step 1:定义结构体啊

struct DNode
{
	int val;
	struct DNode* pre;
	struct DNode* next;
};
typedef DNode* LinkDNode;

初始化略啦!

step 2:插入啊

1.三种插入方法

①头插法:
void insert_head(LinkDNode* head, int val) //头插法
{
    LinkDNode new_node = (LinkDNode)malloc(sizeof(DNode));
    new_node->val = val;
    if (*head == NULL)
    {
        *head = new_node;
        new_node->pre = NULL;
        new_node->next = NULL;
        return;
    }
    LinkDNode tmp = *head;
    *head = new_node;
    new_node->pre = NULL;
    new_node->next = tmp;
}

可以看到,就是多了一步把pre设置为他的前一个结点而已,其他都一样


②尾插法:(一样的啦)
void insert_back(LinkDNode* head, int val) //尾插法
{
    LinkDNode new_node = (LinkDNode)malloc(sizeof(DNode));
    new_node->val = val;
    if (*head == NULL)//如果链表为空
    {
        *head = new_node;
        new_node->pre = NULL;
        new_node->next = NULL;
        return;
    }
    LinkDNode tmp = *head;
    while (tmp->next != NULL)
    {
        tmp = tmp->next;
    }
    tmp->next = new_node;
    new_node->pre = tmp;
    new_node->next = NULL;
}

就多了一个new_node->pre = tmp;


③中间插入(主要就讲一下关键部分啦):
void insert_mid(LinkDNode* head, int target, int val)//中间插入
{
    LinkDNode new_node = (LinkDNode)malloc(sizeof(DNode));
    new_node->val = val;
    if (*head == NULL)
    {
        *head = new_node;
        new_node->pre = NULL;
        new_node->next = NULL;
        return;
    }
    LinkDNode tmp = *head;
    while (tmp->next != NULL && tmp->val != target)
    {
        tmp = tmp->next;
    }
    if (tmp->next == NULL)//如果是尾部
    {
        tmp->next = new_node;
        new_node->pre = tmp;
        new_node->next = NULL;
    }
    else
    {
        LinkDNode tmp2 = tmp->next;
        tmp->next = new_node;
        new_node->pre = tmp;
        new_node->next = tmp2;
        tmp2->pre = new_node;
    }
}

主要讲一下这个部分

  else
    {
        LinkDNode tmp2 = tmp->next;
        tmp->next = new_node;
        new_node->pre = tmp;
        new_node->next = tmp2;
        tmp2->pre = new_node;
    }

想象一下,现在你要插入的结点在两个结点中间呢,然后,是不是这个结点的前驱要指向tmp,后继是不是要指向他前驱(tmp->next)原来的后继?,然后tmp的后继是不是变为new_node了?然后原来tmp的后继的前驱是不是变为new_node了?

插入完结啦!


2.双向列表的浅浅删除

//删除一个结点
void DeleteNode(LinkDNode* head, int target)
{
    if (*head == NULL)
    {
        cout << "The List is empty!" << endl;
        return;
    }
    LinkDNode tmp = *head;
    while (tmp->next != NULL && tmp->val != target)
    {
        tmp = tmp->next;
    }
    if (tmp->next == NULL && tmp->val != target)
    {
        cout << "There is not a node that match the target" << endl;
        return;
    }
    else//将下一个结点的值赋给自己,再将下一个结点删除
    {
        tmp->val = tmp->next->val;
        LinkDNode tmp2 = tmp->next;//记录要删除的后一个结点
        tmp->next->next->pre = tmp;//下个结点的下个结点的上个结点设置为当前节点
        tmp->next = tmp->next->next;//把当前结点的下个结点设置为当前结点的下个
        //结点的下个结点,并且不可以和上面那个顺序颠倒
        free(tmp2);//将删除的结点删除掉
        tmp2=NULL;
    }
}

还是一样一样的,只要解释一下最后面这一部分

    else//将下一个结点的值赋给自己,再将下一个结点删除
    {
        tmp->val = tmp->next->val;
        LinkDNode tmp2 = tmp->next;//记录要删除的后一个结点
        tmp->next->next->pre = tmp;//下个结点的下个结点的上个结点设置为当前节点
        tmp->next = tmp->next->next;//把当前结点的下个结点设置为当前结点的下个
        //结点的下个结点,并且不可以和上面那个顺序颠倒
        free(tmp2);//将删除的结点删除掉
        tmp2=NULL;
    }

想象一下,现在你要删除的结点在两个结点之间,但是我这里写麻烦了,我是按照不能得到前驱写的,把后面一个结点的值赋给tmp,然后删除那个后面的结点了,其实你完全可以直接操作他的前驱后继来操作,交给你们自己啦(我太懒啦)。

我这里的思路是,把当前结点的后继的值覆盖当前结点的值,然后删除后面那个结点,怎么删除?tmp的下一个结点的下一个结点的前驱设置为tmp,tmp的后继设置为tmp原来的下一个结点的下一个结点即可.(着实麻烦啦!麻烦你啦!)

至此,完结


三.单向/双向循环链表

和单向和双向不循环链表的实质一样,只是在插入第一个结点的时候要注意把首尾相接而已,其他的操作都是一模一样的。
拿双向循环链表做例:

void insert_back(DWLinkNode* head, int val) //尾插法
{
    DWLinkNode new_node = (DWLinkNode)malloc(sizeof(DWNode));
    new_node->val = val;
    if (*head == NULL)//如果链表为空
    {
        *head = new_node;
        new_node->pre = new_node;
        new_node->next = new_node;
        return;
    }
    DWLinkNode tmp = *head;
    while (tmp->next != *head)
    {
        tmp = tmp->next;
    }
    new_node->pre = tmp;
    new_node->next = tmp->next;
    tmp->next = new_node;
}

我们来解释一下其中的几个不同点

 if (*head == NULL)//如果链表为空
    {
        *head = new_node;
        new_node->pre = new_node;
        new_node->next = new_node;
        return;
    }

如果链表为空,那么现在要插入的结点将作为链表的第一个结点,此时,我们就要将这个结点的pre和next结点全都作为自己,形成循环结构


那么非空的时候呢 ?

  while (tmp->next != *head)
  {
        tmp = tmp->next;
}

我们从原来的,tmp->next!=NULL,变成了tmp->next!=*head,因为我们的最后一个结点的next不再是空而是第一个结点的地址,所以我们要做出相应的改变


 	new_node->pre = tmp;
    new_node->next = tmp->next;
    tmp->next = new_node;

首先,第一行很好理解,第三行也很好理解,唯一第二行变了,new_node的next要变成tmp->next,即使在最后一个结点也要这么做,因为我们要把new_node的next变成第一个结点(tmp->next);


其他的插入删除操作,都与这里的思想一致,只要多考虑一下每次的插入或者删除对从第一个结点到最后一个结点的这个环考虑即可,不再赘述,自主思考。


四.对链表的一些简单操作(以单链表为例)

1.链表的逆置/反转

方法必然有很多,这里介绍三种比较实用的

①递归法dfs(参数是两个,一个是子结点next,一个是父节点)

首先呢,我们要先弄一个准备函数,因为head为NULL和head->next为空的时候是不需要反转的,不然就正式进行我们的reverse

void reverse_ready(LinkNode& head)
{
	if (head == NULL)
		return;
	if ((head)->next == NULL)
		return;
	LinkNode tmp=head;
	reverse((tmp)->next,tmp);
	tmp->next = NULL;
}

解释一下,为何我们要创建一个tmp变量,而不是直接传head->next和head呢?我们往下先看


接下来,我们要正式反转了,首先我们先dfs一直到最深处(链表尾端),然后依次把cur的next设置为pre即可,注意,在尾端时我们要改变head,因为现在的尾端要变成头了!

void reverse(LinkNode& cur,LinkNode& pre)
{
	//思路,把现在的cur的next指向pre的就行,并且要从最后一个结点开始,所以我们要递归到最深处
	if (cur == NULL)
	{
		head = pre;
		return;
	}
	reverse(cur->next,cur);
	cur->next = pre;
}

ok,这里有一个非常关键的点,非常非常非常关键,因为我们要在cur==NULL时改变head的值,所以我们最开始,在上面的上面那个函数中如果不创建tmp的话,那么我们的第一层dfs,由于传入的是head和head->next的引用(绑定了head和head->next),如果head发生改变,那么其也会相应地发生改变,也就是说,我们刚开始传入的head->next和head,在dfs到最深处把head改变后,也发生了改变,已经不是原来的地址了,所以,我们需要用tmp把原来head记录下来,这样才能把第一个结点的next设置为NULL。

那么问题又来了,为什么我们在reserve中不定义一个tmp把cur或者pre记录下来,他们不会被改变嘛?你想想,我们在reverse中,除最后一个结点,我们只改变了当前的cur的next,然后我们回溯,改变现在的cur的next,每次都是互不影响的。

总体代码:

//1.递归法
void reverse(LinkNode& cur,LinkNode& pre)
{
	//思路,把现在的cur的next指向pre的就行,并且要从最后一个结点开始,所以我们要递归到最深处
	if (cur == NULL)
	{
		head = pre;
		return;
	}
	reverse(cur->next,cur);
	cur->next = pre;
}

void reverse_ready(LinkNode& head)
{
	if (head == NULL)
		return;
	if ((head)->next == NULL)
		return;
	LinkNode tmp=head;
	reverse((tmp)->next,tmp);
	tmp->next = NULL;
}

②迭代法

顾名思义,用三个指针,一个循环即可完成所有操作,如果dfs不熟悉,建议使用这个,很好理解
p3每次都记录下p2->next的现在的值,然后依次往后就行,具体的状况画一张图你们就明白了

在这里插入图片描述
如图,p1 p2 p3的移动和位置关系,其中,蓝色的p1,p2,p3是红色的P1,P2,P3进一次循环后变化而来的

void reverse(LinkNode& head)
{
	//定义三个指针用来迭代
	LinkNode p1 = NULL;
	LinkNode p2 = NULL;
	LinkNode p3 = NULL;
	p1 = head;
	p2 = head->next;
	while (p2 != NULL)
	{
		p3 = p2->next;
		p2->next = p1;
		p1 = p2;
		p2 = p3;
	}
	head->next = NULL;
	head = p1;
}

注意,记得一定要在最后,改变head之前,把head->next=NULL,以满足我们printlist函数中结点的下一个结点为空停止的条件,不然会无限打印。


③就地逆置法

和头插法几乎一模一样,要用到两个指针,一个指针用来保存原来头指针,另一个指针用来迭代

//就地逆置
void reverse(LinkNode& head)
{
	LinkNode p1 = NULL;
	LinkNode p2 = NULL;
	p1 = head;
	p2 = head->next;
	while (p2 != NULL)
	{
		p1->next = p2->next;
		p2->next = head;
		head = p2;
		p2 = p1->next;
	}
}

我们每次都改变p->next,以此从第二个节点向第N个结点推进,每次前进一个结点
与迭代法不一样的是,我们每次改变的是head,而不是p,p一直都是原来head的值,没有变过,p就是用来让p2往后走的,并没有参与值的交换,真正交换迭代的是head和p2,在循环结束后,head是next为NULL时的p2,也就是原来列表的尾结点,循环完,head也更新完了。

2.冒泡排序

相信正常版本的冒泡排序大家应该都烂熟于心了吧,那么我们的关键,就是要找出一共有几组冒泡排序(如果你不记录链表大小的话)
先上代码

//升序排列
void bubble_sort_list(LinkNode head)
{
	LinkNode p1 = NULL;
	LinkNode p2 = NULL;
	LinkNode p3 = head;
	do
	{
		p1 = head;
		p2 = head->next;
		//一轮冒泡排序
		while (p2!=NULL&&p2 != p3)
		{
			if (p1->val > p2->val)
			{
				int num = p1->val;
				p1->val = p2->val;
				p2->val = num;
			}
			p1 = p2;
			p2 = p2->next;
		}
		p3 = p1;
	}while(p3!=head)
}

解释:
一共有两个while,里面的while显然是一组冒泡排序,然后外层while是循环指定组数的冒泡排序,那么怎么找到外层while的条件呢?我们定义一个p3,当p3!=head的时候就停,什么意思?先往下走,内层while的条件,p2为空就停很好理解,因为到最后了嘛,那为什么p2=p3也停呢?我们先看p3的变化是怎样的,p3随便初始化一下(因为我们会更新),进去了以后,我们必须更新p3的值,我们的更新是在外层循环之内,内层循环之外,假设我们一共有n个结点,也就是说,我们一次更新,p3变为了第n个结点,第二次更新,p3变成了第n-1个结点…依次循环,是不是就是数组的冒泡排序一样的道理?最后,当p3等于head了,也就是排序完了,也就结束了,思路在此。


3.快速排序

老规矩,先上代码

//快速排序
void quick_sort_list(LinkNode& head, LinkNode& tail)
{
	if (head == tail) return;
	LinkNode i = head;
	LinkNode j = head->next;
	int val = head->val;
	while (j != tail)
	{
		if (j->val > val)
		{
			i = i->next;
			swap(i->val, j->val);
		}
		j = j->next;
	}
	swap(head->val, i->val);
	quick_sort_list(head,i);
	quick_sort_list(i->next, tail);
}

鉴于快速排序很多人都不懂,所以这里详细讲述。
一般快速排序的原理:1.找基准数
2.在一个基准数的左边都是比他小的数,右边的都是比他大的数
3.再用分治思想处理这个基准数的两边(因为左边都是比他小的数但不一定按顺序小的啊,比如基准数是5,左边不一定是1,2,3,可能是3,1,2,所以要分治)

链表快速排序的原理:1.把head直接作为基准数即可
2.tail不是尾结点,而是尾结点的下一个结点(即.end())

step 1:初始化两个指针i,j,分别为你要处理的部分的第一和第二个结点。
step 2:j依次往后,如果一旦不符合划分条件(与基准数比较),就把i往后移一个并且和j的值交换
step 3:循环结束时的i的位置就是基准数该在的位置,然后swap一下head和i的值
step 4:分治思想,分别快速排序左边和右边


4.合并两个有序列表

先上代码

LinkNode MergeLinkList(LinkNode a, LinkNode b)
{
	if (!a)
		return b;
	if (!b)
		return a;
	LinkNode head = NULL;
	LinkNode tail = NULL;
	LinkNode l1 = a;
	LinkNode l2 = b;
	if (a->val < b->val)
	{
		head = a;
		tail = a;
		l1 = l1->next;
	}
	else
	{
		head = b;
		tail = b;
		l2 = l2->next;
	}
	while (l1 && l2)
	{
		if (l1->val <= l2->val)
		{
			tail->next = l1;
			tail = l1;
			l1 = l1->next;
		}
		else
		{
			tail->next = l2;
			tail = l2;
			l2 = l2->next;
		}
	}
	if (l1)
		tail->next = l1;
	if (l2)
		tail->next = l2 ;
	return head;
}

一步一步说思路
step1:判断特殊情况 l1为空的时候返回l2,l2为空的时候返回l1

step2:定义四个指针(或者直接使用a,b,只要定义两个指针了),这里为了方便理解,定义四个指针,一个是第一个a链表的头指针,一个是第二个b链表的头指针,然后一个head指针用来存放新链表的头指针,一个tail用来存放新链表的尾指针。

step3:初始化head和tail,选a,b第一个结点的最小值作为头,同时更新尾,然后还要更新l1或者l2,因为新链表中已经保存了l1/l2中的结点了。

step4:循环操作,条件是l1,l2都不为空就进行,因为谁为空了,代表一个链表已经遍历完了。

step5:把剩余的链表的剩下部分接到尾部。

step6: 返回这个头结点。

注意:因为我们是操作指针,所以我们合并他们会改变原来链表的模样。


5.查找中间结点

pair<LinkNode,LinkNode> FindListMid(const LinkNode& head)
{
 if(head==NULL) return pair<LinkNode, LinkNode>(NULL, NULL);
//定义两个指针,一个慢,一个快
 LinkNode low = head;
 LinkNode quick = head;
 while (quick!=NULL)
 {
	 quick = quick->next;
	 if (quick != NULL)//结点数为偶数
	 {
		 quick = quick->next;
	 }
	 else//节点数为奇数
	 {
		 return pair<LinkNode,LinkNode>(low,NULL);
	 }
	 if(quick!=NULL)
	 low = low->next;
 }
 return pair<LinkNode,LinkNode>(low,low->next);
}

对于这个函数,我的选择是返回一个pair,因为查找的中间结点可能不止一个(结点个数为偶数个),如果为奇数个就第二个返回NULL。
解释一下

step1:因为我们是查找头结点,所以我们把参数变成const,或者直接不传入引用,这里选择用const修饰。

step2:如果传入的head为空,那么直接返回两个空指针。

step3:定义两个指针,一个快指针(一次走两步),一个慢指针,两者同时从head出发,当quick变成NULL的时候,证明已经到尾了,这个时候i的值便是中间值。

PS:while里,先让quick走一步,判断一下,再走第二步,如果第一步就到NULL了,证明为偶数个结点,比如(1->2),否则就为奇数个结点,奇数的时候,返回low和NULL,否则就返回low和low->next,这里有个问题,为什么low要比quick后走,理论说比较抽象,我们举个例子:
1-2-3-4-5-6
显而易见,中间结点为3 4
low:1
quick:1

low:1 2
quick:1 2 3

low:1 2 3
quick:1 2 3 4 5

//此时若不更新low
low:1 2 3
quick 1 2 3 4 5 6
//此时若更新low
low:1 2 3 4
quick 1 2 3 4 5 6

会发现,low并没有指向3而是4了,所以quick要先走,如果不满足条件直接停就好。
或者你可以这么理解,让quick走x步,而low是走x/2步,所以,只有quick走了两步还没事的情况下更新low。


差点忘记了,还有两个很重要的东西,打印和销毁链表

6.打印链表

传入头指针,打印即可


void PrintLinkList(const LinkDNode* head)//打印链表
{
    if (*head == NULL)
    {
        cout << "The List is empty!" << endl;
        return;
    }
    LinkDNode tmp = *head;
    while (tmp != NULL)
    {
        cout << tmp->val << " ";
        tmp = tmp->next;
    }
    cout << endl;
    return;
}

7.删除链表

传入头指针,循环删除即可
注意,我们是直接操作*head了,没有搞一个tmp,因为我们既然删除这个链表了,这个头指针就没必要还存着啦!


删除链表
void DeleteLink(LinkNode* head)
{
	if (*head == NULL)
	{
		cout << "The LinkList is empty !" << endl;
		return;
	}
	while ((*head)!= NULL)
	{
		LinkNode tmp = *head;
		*head = (*head)->next;
		free(tmp);
}
}

8.查找倒数第k个结点

与查找中间节点一个原理,先定义一个快指针,多走K步,然后再快慢指针同时移动,以相同的速度,等quick到链表的尾后的时候,low在的位置就是要查找的结点。

理解:因为quick会到尾后处,所以quick和low的距离k的意义就是倒数第k个的意思。

 LinkNode find_last_k(const LinkNode& head, int k)
 {
	 if (head == NULL) return NULL;
	 LinkNode low = head;
	 LinkNode quick = head;
	 while (k--)
	 {
		 quick = quick->next;
		 if (quick == NULL) return NULL;
	 }
	 while (quick != NULL)
	 {
		 low = low->next;
		 quick = quick->next;
	 }
	 return low;
 }

step 1:检查,如果head为空直接返回

step 2:定义快慢指针

step 3:让quick多走k步,如果quick先变为NULL 说明不存在

step 4:再让quick和low同步走,知道quick为尾后

step 5:返回low


9.判断单链表是否带环

上代码

LinkNode CircleListJudge(LinkNode head) //O(n)
{
	if (head == NULL) return NULL;
	LinkNode low = head;
	LinkNode quick = head;
	do
	{
		quick = quick->next;
		if (quick != NULL)
			quick = quick->next;
		low = low->next;
	} while (quick != low && quick && low);
	if (quick == NULL || low == NULL) return NULL;
	return low;
}

解释一下:
核心思想:定义两个快慢指针,如果链表不带环,那么慢指针是永远追不上快指针的,但是如果有环,那么快指针是一定会套圈慢指针的,也就是说一定会相遇。那么相遇我们就返回相遇结点,否则返回NULL。

step 1:如果链表为空就返回NULL

step 2:定义两个指针,一个慢指针,一个快指针

step 3:while的条件,quick!=low,或者quick为空了,因为如果没环,quick一定比low先为空,所以写不写low为空的判定条件都无所谓。

step 4:判断循环是怎么结束的,如果是quick==NULL 就返回NULL,否则返回当前low(quick)的值作为相遇的结点.


10.求出带环链表的长度

int CircleListLength(LinkNode meet) //O(n)
{
	int counter = 1;
	LinkNode start = meet;
	while (start->next != meet)
	{
		counter++;
		start = start->next;
	}
	return counter;
}

解释一下:
核心思想:定义一个counter=1(包括了当前结点),设置一个指针,从meet的点开始,往后走,每次counter++,走一圈再回来的时候,停下,返回counter便是长度,很好理解所以不做过多解释。


11.求出带环链表环的进口

LinkNode CircleListEnter(LinkNode head, LinkNode meet) //O(n)
{
	LinkNode start = head;
	while (start != meet)
	{
		start = start->next;
		meet = meet->next;
	}
	return meet;
}

解释一下:
核心思想:一个数学问题

数学问题: 假设low指针一次走x步,quick一次走2x步,设从head开始到进口的长度为L,设相遇点到进口的距离为x,进口到相遇点的距离为d(环的长度为x+d,只要走x+d就能走完一圈)

2*(L+d) 【慢指针走的】= L+n(x+d) +d ;

整理可得 L = x + (n-1)*(x+d);
也就是说,L为x再加上(n-1) 圈环的长度
也就是说,从head开始的指针,与从相遇点开始的指针会在进口处相遇


12.

五.实战演练

约瑟夫环

n 个人的编号是 1~n
如果他们依编号按顺时针排成一个圆圈,从编号是 1 的人开始顺时针报数
当报到k 的时候,这个人就退出游戏圈。下一个人重新从 1 开始报数。
求最后剩下的人的编号。这就是著名的约瑟夫环问题。
本题目就是已知 n k 的情况下,求最后剩下的人的编号。

#include <bits/stdc++.h>
using namespace std;
int n, k;

struct Node//链表
{
	Node(int val) :id(val) { next = NULL; pre = NULL; };
	int id;
	struct Node* next;
	struct Node* pre;
};

typedef Node* LinkNode;

void insert(LinkNode& head,int id)//考虑头插法速度快
{
	LinkNode new_node = new Node(id);
	if (head == NULL)
	{
		head = new_node;
		new_node->pre = new_node;
		new_node->next = new_node;
	}
	else
	{
		new_node->next = head;
		new_node->pre = head->pre;
		head->pre->next = new_node;
		head->pre = new_node;
		head = new_node;
	}
}

int Deletek(LinkNode& start,int k)
{
	if (start->next == start&&start->pre ==start&&start!=NULL) return (start->id);
	k -= 1;//为何要-1,因为第一个数已经占了一个名额啦
	while(k--)
	{
		start = start->next;//找到那个编号
	}
	//删除这个结点
	LinkNode& pre = start->pre;
	LinkNode& next = start->next;
	if (pre == NULL) start->pre = start;
	if (next == NULL) start->next = start;
	pre->next = next;
	next->pre = pre;
	LinkNode tmp = start;
	start = start->next;
	delete(tmp);
	return 0;
}

int main()
{
	cin >> n >> k;
	LinkNode listhead = NULL;
	for (int i = n; i >= 1; i--)
	{
		insert(listhead,i);
	}
	int idx=0;//保存那个剩下的人的编号
	LinkNode start = listhead;
	do
	{
		idx = Deletek(start,k);
	} while (!idx);

	cout << idx;

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值