单链表反转
方法一:
考虑如何将一个单链表反转,一开始的灵感是由头插法建立单链表算法而来的,即事先建立一个空的单链表,在遍历单链表的同时,不断的将结点从头插入到空链表中,这样一来,就建立出了单链表的反转链表。
算法思路:
- 建立新的空链表,令指针q指向空链表的头节点
- 遍历单链表,提取每次访问结点的数据域
- 创建新的Node结点,将提取的结点data数据赋值给新结点的data数据域
- 令新结点的指针指向头节点的后继结点,再将头节点的后继指针指向新结点,直到遍历结束
时间复杂度O(n),空间复杂度O(n)
具体实现代码如下:
List list; // 已知链表list
List new_list; // 新建空链表new_list
Node *p = list.head();
Node *q = new_list.head();
while (p->next != NULL) // 遍历链表list
{
Node *s = new Node; // 建立新的结点并初始化
s->data = p->data;
s->next = q->next; // 将新结点插入到空链表首部
head->next = s;
p = p->next;
}
这样便得到了新的链表new_list,即原链表list的反转链表。
方法二:
在头插法创建新链表的基础上,设计出在链表本身上进行的原地反转算法。
算法思路:
- 采用三个结点指针,head、prev、pcur,分别指向头节点,处理结点的前继结点,待处理结点
- 以第二个结点开始作为待处理结点,进行反转
- 令prev的后继结点指向pcur的后继结点,再令pcur的后继结点指向head的后继结点,令head的后继结点指向pcur结点,最后重新指定新的pcur结点为prev的后继结点
- 重复第三步,直至待处理结点为空
时间复杂度O(n),空间复杂度O(1)
具体实现代码如下:
void MyList::ReverseList()
{
if (head->next == NULL || head->next->next == NULL) // 特殊情况处理
return;
Node *prev = head->next; // 初始化反转结点的前继指针
Node *pcur = prev->next; // 初始化需要反转的结点指针
while (pcur != NULL)
{
prev->next = pcur->next;
pcur->next = head->next;
head->next = pcur;
pcur = prev->next;
}
}
两个有序的链表合并
首先我们分析两个有序链表,这里举例 list1 = { 1, 3, 5, 7 }, list2 = { 2, 4, 6, 8 }。其中pHead1和pHead2分别指向两个链表的首结点,接着我们分析两个链表合并后的头指针pMergedHead,通过比较两个链表的头结点的data值可知,list1链表的头结点data值小于list2的头结点data值,因此确定出合并后链表的头指针为pHead1。紧接着确定list1的子链表与list2链表合并后的头指针,依次递归,直至两个链表中任意链表为空。
因此总结出递归算法的思路:
- 每次递归过程返回指向两个链表合并后的头结点的指针pMergedHead
- 令上次递归得到的pMergedHead指针指向此次递归过程的结果
由于每次递归都会使得两条链表中的一条减少一个结点,因此当某条链表为空时,会返回另一条链表的头结点,此时到达递归的基准情形。
时间复杂度O(min(m, n)),空间复杂度O(min(m, n))
具体代码实现如下:
Node* Merge(Node* pHead1, Node* pHead2)
{
if (pHead1 == NULL) // 递归的基准情形
return pHead2;
else if (pHead2 == NULL)
return pHead1;
Node* pMergedHead = NULL; // 定义合并后链表的头指针
if (pHead1->data < pHead2->data) // 分情况递归
{
pMergedHead = pHead1;
pMergedHead->next = Merge(pHead1->next, pHead2);
}
else
{
pMergedHead = pHead2;
pMergedHead->next = Merge(pHead1, pHead2->next);
}
return pMergedHead;
}
快慢指针的应用
由于快慢指针在链表的一些操作中有很大的帮助,这里先介绍一下快慢指针的概念。
快慢指针中的快慢指的是移动的步长,即每次向前移动速度的快慢。例如可以让快指针每次沿链表向前移动2,慢指针每次向前移动1次。我们在链表的某些操作时,可以定义两个指针来遍历链表,通过使两个指针的移动速度不同,来使操作变得更加简单。
快慢指针应用(一):判断链表是否存在环
如果链表存在环,就好像操场的跑道是一个环形一样。此时让快慢指针都从链表头开始遍历,快指针每次向前移动两个位置,慢指针每次向前移动一个位置;如果快指针到达NULL,说明链表以NULL为结尾,没有环。如果快指针追上慢指针,则表示有环。
时间复杂度O(n),空间复杂度O(1)
具体代码实现如下:
bool HasCircle(Node *head)
{
if (head == NULL)
return false;
Node *slow = head, *fast = head;
while (fast != NULL && fast->next!=NULL) // 注意循环结束的条件
{
slow = slow->next; // 慢指针每次前进一步
fast = fast->next->next; // 快指针每次前进两步
if(slow == fast) // 相遇,存在环
return true;
}
return false;
}
快慢指针应用(二):判断链表是否存在环,如果存在,找到环入口
之前已经介绍了如何判断链表是否存在环,接下来分析如果链表存在环,如何找到环的入口点?
当fast若与slow相遇时,slow肯定没有走遍历完链表或者恰好遍历一圈。于是我们从链表头与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点。(这里有必要给出图文解释,以后会补充)
时间复杂度O(n),空间复杂度O(1)
具体代码实现如下:
Node* findLoopPort(Node *head)
{
Node *fast, *slow;
fast = slow = head;
// 第一步:判断链表是否存在环
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast) // 链表存在环
{
break;
}
}
if ((fast == NULL) || (fast->next == NULL)) // 链表不存在环
{
return NULL;
}
// 第二步:寻找环的入口点
slow = head; // 让slow回到链表的起点,fast留在相遇点
while (slow != fast) // 当slow和fast再次相遇时,那个点就是环的入口点
{
slow = slow->next; // 此时快慢指针每次都只走一步
fast = fast->next;
}
return slow;
}
删除链表中倒数第n个结点
可以定义两个指针,第一个指针从链表的头指针开始遍历向前走k-1步,第二个指针保持不动;从第K步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个指针到达链表的尾节点时候,第二个指针正好是倒数第K个节点。
时间复杂度O(n),空间复杂度O(1)
具体代码实现如下:
bool RDelete_N(Node *pHead, unsigned n) // 函数名前面的R代表反向
{
if (n == 0 || pHead == NULL) // 这里k的计数是从1开始的,若k为0或链表为空返回NULL
return NULL;
Node * pAhead = pHead; // 初始化快指针和慢指针
Node * preBehind = pHead;
for (int i = 0; i < n; ++i)
{
pAhead = pAhead->next;
if (pAhead == NULL)
return false; // 当链表长度小于n时候,返回false
}
while (pAhead->next != NULL) // 前后两个指针一起向前走,直到前面的指针指向尾结点
{
preBehind = preBehind->next;
pAhead = pAhead->next;
}
// 此时指针preBehind指向倒数第n个结点的前继结点
// 开始删除操作
Node *pBehind = preBehind->next;
preBehind->next = pBehind->next;
delete pBehind;
return true;
}
求链表的中间结点
令慢指针每次移动一个位置,而快指针每次移动两个位置,由于快指针移动速度是慢指针的两倍,所以当慢指针指向链表尾结点时,慢指针刚好走到链表的中间。但是根据链表结点的个数要进行分类讨论(下面讨论以链表不存在头结点为前提):
- 当链表结点个数为奇数时
此时快指针走到尾结点为终止条件,直接返回慢指针即可。
- 当链表结点个数为偶数时
此时快指针达到倒数第二个结点为终止条件,我们这里选择返回链表中间两个结点的前面那个。
时间复杂度O(n),空间复杂度O(1)
具体代码实现如下:
Node* GetMiddle(Node *head)
{
Node *fast, *slow; // 初始化快慢指针指向第一个结点
fast = slow = head;
while (fast && slow)
{
if (fast->next == NULL)
return slow;
else if (fast->next != NULL && fast->next->next == NULL)
return slow; // 直接返回slow指针,另一种情况则是:return slow->next;
else
{
fast = fast->next;
fast = fast->next;
slow = slow->next;
}
}
}