有关单链表的一些问题总结


链表是一种非常重要的数据结构,在笔试和面试时经常会遇到,所以自己总结了一下。

本来两天前就该写好的,只是一直在想其他的事。最近两天感觉很累,这雪已经下了两天了,顶风冒雪的奔波,本想有个好结果,但好像又是一场空。今年为什么什么事都不顺,事业、感情,都是他妈的一塌糊涂,真想骂两句,可是就不知道骂谁,一切也只能骂自己!

1.单链表定义

回归正题。链表的定义,我摘自维基百科。重复看一下吧。“链表Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。”链表与数组相比的优缺点:使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

2.单链表相关问题

链表的结构定义如下代码,为了在后面测试使用,我写了一个利用数组来生产一个链表的函数。代码如下:
typedef struct LinkedList
{
	int data;
	LinkedList *next;
}node;

node *CreateList(int data[],int len)
{
	node *head = new node;
	head->data = data[0];
	head->next = NULL;
	node *p = head;
	for(int i = 1; i < len; ++i)
	{
		node *tmp = new node;
		tmp->data = data[i];
		tmp->next = NULL;
		p->next = tmp;
		p = tmp;
	}
	return head;
}

2.1 单链表的遍历

由于链表不像数组那么可以直接根据下标来遍历值。如果想要在链表中找到某个值,必须依次遍历链表,最坏时要遍历所有节点,所以单链表的遍历时间复杂度为O(n)。如果我们想遍历单链表,一个主要的依据就是单链表的尾节点的next是空,我们根据这个性质就可以依次遍历单链表了。代码如下:
// 遍历链表
void print(node *head)
{
	if(head == NULL)
	{
		cout << "List is empty!" << endl;
		return;
	}
	node *p = head;
	while(p)
	{
		cout << p->data << " ";
		p = p->next;
	}
}

2.2 删除给定头结点单链表中指定的值

对于删除操作,在单链表中,影响最大就是单链表的指针域,毕竟,单链表只有靠指针域才能找到下一个节点在内存中的位置。在这里,我们没有考虑删除链表中重复的几个数。删除指定值的节点,首先要找到这个节点,同时要记录下此节点的前驱结点,因为我们要修改前驱节点的指针域,以保证链表的连续。找到此节点后,我们要考虑三种情况。第一种就是此节点是头结点。我们不能简单的删除头结点,因为对于这个链表我们只有头结点的信息。可以将头结点的后继节点复制给头结点,并删除后继节点,此时就相当于删除了原头结点。第二种情况就是此节点为尾节点。此时,比较简单,我们直接将前驱节点的指针域赋值为空,然后删除尾节点即可。第三种情况是为中间节点。此时,我们可以将此节点的指针域赋给前驱结点的指针域,然后删除此节点。具体代码如下:
// 给出头结点,删除指定的值
void DeleteNode(node *head,int num)
{
	if(head == NULL)
	{
		cout << "List is NULL.";
		return;
	}
	node *p = head;
	node *q ;
	while(p)
	{
		if(p->data != num)
		{
			q = p;	// p的前驱节点
			p = p->next;
			if(p == NULL)
			{
				cout << "未找到此节点!";
				break;
			}
		}
		else
			break;
	}

	if(p == head)	// p为头结点
	{
		q = head->next;
		if(q == NULL)
		{
			head = NULL;
			delete head;
		}
		head->data = q->data;
		head->next = q->next;
		delete q;
		
	}
	else if(p->next == NULL)	// p为尾节点
	{
		q->next = NULL;
		delete p;
	}
	else	// p为中间节点
	{
		q->next = p->next;
		delete p;
	}
}

2.3 在一个无头节点的链表中,给定一个随机的节点指针(不是第一个,也不是最后一个)删除它

此时的问题,不同于上面的删除,此时没有头结点,所以你找不到要删除节点的前驱节点。现在只有当前节点的一个指针。我们根据这个指针只能找到它的后继节点。因为无法修改前驱节点的指针域,我们就变通的去实现。现在有后继节点,可以将后继节点赋给此节点,那么此时这个节点就是它的后继节点,然后删除后继节点,这样就实现了删除随机节点。代码如下:
void DeleteRandomNode(node *p)
{
	if(p == NULL)
		return;
	node *q = p->next;
	if(q != NULL)
	{
		p->next = q->next;
		p->data = q->data;
		delete q;
	}
}

2.4 顺序翻转单链表

翻转单链表,其实也就是将指针域倒过来。重点是,我们既要让原链表顺序向后走,又要修改指针域,所以我们需要记录一些指针域的值,以保证上面的操作。不多说了,直接上代码:
// 顺序翻转链表
node* Reverser(node *head)
{
	if(head == NULL)
		return NULL;
	node *p1 = head;
	node *p2 = head->next;
	while(p2)
	{
		node *p3 = p2->next;
		p2->next = p1;
		p1 = p2;
		p2 = p3;
	}
	head->next = NULL;
	return p1;
}

2.5 在节点p后面插入一个节点

在节点p后面插入一个节点,很直接,即只要修改p和新增节点的指针域即可。直接上代码:
// 在节点p后面插入一个节点
void InsertNodeAfterP(node *p,int num)
{
	node *t = new node;
	t->data = num;
	if(p->next == NULL)
	{
		p->next = t;
		t->next = NULL;
	}
	else
	{
		t->next = p->next;
		p->next = t;
	}
}

2.6 在节点p前面插入一个节点

在节点p前面插入一个节点,相对于上面的问题,需要多想一步。在p前面插入一个节点,因为无法得知前驱节点,所以不能直接修改指针域。所以,我们只能将新增节点插入到p后面,那么如何符合题意呢?既然能插入到p后面,那么现在只是顺序不一样,所以我们颠倒一下这两个节点不就符合题意了嘛。颠倒,也就是将值交换一下。代码如下:
// 在节点p前面插入一个节点
void InsertNodeBeforeP(node *p,int num)
{
	InsertNodeAfterP(p,num);
	node *q = p->next;
	q->data = p->data;
	p->data = num;
}

2.7 链表的排序

链表的排序,比数组中排序复杂一点,主要是涉及到了指针域的操作,所以有些排序算法不实用与链表中,比如快速排序算法不适合链表的排序。链表的排序最好的应该就是归并排序,因为没有生产新的节点,辅助空间为O(1),时间复杂度为O(nlogn)。冒泡排序可以用到这里,这里会有不可避免的值的交换。以下代码中,冒泡排序是自己写的,归并排序是在别的地方抄下来的,并没有测试。
归并排序:
node *linkedListMergeSort(node *pHead)
{
	int len = getLen(pHead);
	return mergeSort(pHead,len);
}
node *mergeSort(node *p,int len)
{
	if(len == 1)
	{
		p->next = NULL;
		return p;
	}
	node *pmid = p;
	for(int i = 0; i < len/2; i++)
	{
		pmid = pmid->next;
	}
	node *p1 = mergeSort(p,len/2);
	node *p2 = mergeSort(pmid,len-len/2);
	return merge(p1,p2);
}
node *merge(node *p1,node *p2)
{
	node *p = NULL,*ph = NULL;
	while(p1 != NULL && p2 != NULL)
	{
		if(p1->data < p2->data)
		{
			if(ph == NULL)
			{
				ph = p = p1;
			}
			else
			{
				p->next = p1;
				p1 = p1->next;
				p = p->next;
			}
		}
		else
		{
			if(ph == NULL)
			{
				ph = p = p2;
			}
			else
			{
				p->next = p2;
				p2 = p2->next;
				p = p->next;
			}
		}
	}
	p->next = (p1 == NULL) ? p2 : p1;
	return ph;
}

冒泡排序:
// 链表排序
void SortList(node *head)
{
	if(head == NULL || head->next == NULL)
		return;
	node *p = head;
	int len = 0;
	while(p)
	{
		len++;
		p = p->next;
	}

	p = head;
	int num = 0;// 记录比较的次数
	while(len)
	{
		while(p->next)
		{
			if(p->data > p->next->data)
			{
				int tmp = p->data;
				p->data = p->next->data;
				p->next->data = tmp;
			}
			p = p->next;
			num++;
			if(num == len)
				break;
		}
		p = head;
		num = 0;
		len--;
	}
}

2.8 只遍历一次找到中间节点

对于此问题,如果去掉只遍历一次的限制,我们可以先遍历以下链表记录下链表的长度,然后折半,就可以找到中间的节点,但是此时已经遍历第二次了。我们可以设置两个指针,都从头结点开始遍历,一个节点每次走一步,另一个每一次走两步,那么当后一个节点到达末尾时,前面的那个节点岂不是正好到达中间!
node* SearchMid(node *head)
{
	node *p1 = head;
	node *p2 = head;
	while(1)
	{
		if(p1->next != NULL && p2->next->next != NULL)
		{
			p1 = p1->next;
			p2 = p2->next->next;
		}
		else
			break;
	}
	return p1;
}

2.9 输入一个单向链表,输出该链表中倒数第k个节点。链表的倒数第0个节点为链表尾指针

此题为上面的一个普遍形式吧。思路一:首先遍历以下链表得到链表的长度,然后从头开始遍历len-k个节点,就是倒数第k个节点。思路二:设置两个指针p1和p2.p1先从头结点开始遍历链表,遍历k个节点。然后,p2从头节点开始遍历,p1从当前位置开始遍历,当p1到达末尾时,p2就到达了倒数第k个节点。
node *LastK1(node *head,int K)
{
	if(head == NULL)
		return NULL;
	node *p = head;
	int len = 0;
	while(p != NULL)
	{
		len++;
		p = p->next;
	}
	p = head;
	int num = len - K;
	while(p)
	{
		num--;
		if(num == 0)
			break;
		p = p->next;
	}
	return p;
}
node *LastK2(node *head,int K)
{
	if(head == NULL)
		return NULL;
	node *p1 = head;
	node *p2 = head;
	while(K+1)
	{
		K--;
		p1 = p1->next;
	}
	while(p1 != NULL)
	{
		p1 = p1->next;
		p2 = p2->next;
	}
	return p2;
}


2.10 判断一个单链表是否有环

思路:使用两个指针p1,p2.从头开始遍历,p1每次前进一步,p2每次前进两步。如果p2到达了链表尾部,说明无环,否则p1 p2必然会在某个时刻相遇,从而检测到链表中有环。
// 判断是否有环
bool IsCycle(node *head)
{
	if(head == NULL)
		return false;
	node *p1 = head;
	node *p2 = head;

	while(p1->next)
	{
		p1 = p1->next;
		p2 = p2->next->next;

		if(p1 == p2)
		{
			break;
			return true;
		}
		else
			continue;
	}
	
	if(p1->next == NULL)
		return false;
	else
		return true;
}

2.11 给定两个单链表,检测两个链表是否有交点,如果有返回第一个交点

思路1:将两个链表首尾相连,合并成一个链表,如果有交点,那么就会有环。从而判断出是否有交点。要找第一个交点,可以首先得到两个链表的长度,len1,len2,假设len1>len2,那么p1先向前走len1-len2步,然后p1和p2同时前进,当p1==p2时,那就是第一个交点。
思路2:两个链表相交,那么只有Y字形状的。所以只要顺序链表两个链表到尾端,如果尾端相同,那么就是有交点,否则就没有交点。
node *IsHaveCrossingNode1(node *head1,node *head2)
{
	node *p1 = head1;
	node *p2 = head2;
	node *p3 = head2;
	while(p2->next)
		p2 = p2->next;
	p2->next = p1;	// p1 p2合并成一个链表
	bool mask = IsCycle(p3);
	p2->next = NULL;	// 断开链表
	p1 = head1;
	p2 = head2;
	int len1 = 0;
	int len2 = 0;
	while(p1)
	{
		len1++;
		p1 = p1->next;
	}
	while(p2)
	{
		len2++;
		p2 = p2->next;
	}
	p1 = head1;
	p2 = head2;

	if(mask)	// 有交点时,求交点
	{
		if(len1 > len2)
		{
			int n = len1 - len2;
			while(n-1)
			{
				p1 = p1->next;
				n--;
			}
			while(p1->next!= NULL && p2->next != NULL)
			{
				p1 = p1->next;
				p2 = p2->next;
				if(p1 == p2)
				{
					break;
					return p1;
				}
			}
		}
	}
	else
	{
		return NULL;
	}

}


bool IsHaveCrossingNode(node *head1,node *head2)
{
	if(head1 == NULL || head2 == NULL)
		return false;
	node *p1 = head1;
	node *p2 = head2;
	while(p1->next)
	{
		p1 = p1->next;
	}
	while(p2->next)
	{
		p2 = p2->next;
	}

	if(p1 == p2)
		return true;
	else
		return false;
}

Ok,That's all!目前只总结了这些问题,如果以后遇到新问题再更新吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值