数据结构之链表与数组(二) -单向链表上的简单操作问题
本文主要介绍一些解决单向链表上部分操作问题的思路和代码实现。
主要的问题包括以下几点:
1 向单向链表中插入一个节点
2 删除单向链表中的一个节点
3 查找单向链表中的一个节点
扩展问题1:查找单向链表中的倒数第k个节点。
扩展问题2:查找单向链表中的中间节点,当节点总个数为偶数时返回中间两个元素中的前者(后者)
4反转单向链表(非递归实现)
5反转单向链表(递归实现)
6判断单向链表是否有环
7判断两个单向链表是否相交
扩展问题:返回两个链表的第一个交点。
8 用单链表实现栈,要求push和pop的时间复杂度为O(1)
9 用单链表实现队列,要求enQueue和deQueue的时间复杂度为O(1)
10 在一个链表中删除另一个链表中的元素(即求差集(A-B))
由上一篇文章可知,从数据的底层存储角度来讲,数据结构可分顺序表(数组)和链表两种。因此,要掌握好数据结构,首先就要掌握好对数组和链表的操作。
在本篇文章中,主要先介绍一些链表的基本操作。
一个链表又可以有好多种表现形式,它可以是单向的或双向的,也可以是排序的或者未排序,或者是环形的和非环形的。下面主要介绍一些链表的常见的操作。
在解决生活或计算机中的任何问题时“三思而后行”的道理都是非常实用的。只有想明白了,要做什么、怎么做,然后再着手去做,才能比较容易地解决问题。如果连要做什么,怎么做都不清楚,那现在所做的行动能成功的概论就非常小了。因此,要用计算机编码来解决一些问题的时候,不要急于编码,而是要先想清楚思路和注意点,然后再着手实现自己的思路。
在处理链表问题时的公共注意点有:
1,要时刻考虑指针是否为空
2,不要把指针指向一些无效的地址空间。
3,如果没要求,操作的过程中尽量不要破坏链表的原始结构;如果破坏链表的结构是目前较好的一种实现方式,那么处理完数据后,一定要记得还原链表的原始数据结构。
下面介绍一些常见的单向链表上的操作问题。
单向链表节点定义:
class SingleList
//单向链表上的节点定义
class SingleList
{
public:
int data;
SingleList *next; //指向下一个结点的指针
SingleList()
{
next = NULL;
}
};
1 向单向链表中插入一个节点
思路:如图1所示,向链表中插入一个节点。
图1 插入节点
如果已知一个节点指针pre和一个节点指针cur,要把cur插入到pre节点之后,很显然要保证链表不会断开而丢失后面的节点,要先把后面的节点指针(指向lat的指针)保存下来,即有cur->next = pre->next,然后把cur连接的一串链表连接到pre后面,即pre->next = cur;
上面介绍了,在一个节点之后插入节点的情况。这是通常的情况。如果要向一个链表的头部插入节点,就只需要将新节点的下一个指针指向链表的头指针即可。
在这种情况下,有两点要注意:
1,链表是否为空链表
2,要插入的节点是不是空指针。
代码实现:
//向单链表中插入一个节点(插入在链开始处)
//输入参数:单链表的头指针和要插入的节点指针
//输出参数:无
//返回值:指向单链表的头指针
SingleList* Insert(SingleList *head,SingleList *node)
{
if(node == NULL)
{
return head;
}
else if(head == NULL)
{
return node;
}
node->next = head;
head = node;
return head;
}
2 删除单向链表中的一个节点
思路:
图2 删除节点cur
如图2所示,要删除节点cur就像要在环环相扣的链上去掉一环一样, 我们只需要将前面的环和当前环所连接的下一个环相连就可以了。
针对这情况,我只需要从头遍历链表,找到当前节点的前一节点pre,然后令pre->next = cur->next。因为要从头遍历链表所以删除操作的时候时间复杂度是O(n)。
但是,在有些情况下,如果要求时间复杂度必须为O(1),我们又能怎么做呢?想一下,如果要求时间复杂度为O(1)的话,肯定不能再遍历链表,而是在当地做几个小步聚,那么做什么才能删除呢?
如果不能找到cur的前一个节点,那我们是不是可以像数组中的覆盖一样用后一个节点的值把当前节点值覆盖,然后删除后一个节点呢。当然可以,如图3所示。但是,这种方法存在一个局限,就是如果要删除的节点是尾节点的话,即没有可以替代它被删除的点时,我们就只能按上面的循环遍历查找前一个节点了。
图3 删除节点cur(不知道头指针时)
在删除链表中的节点时,要特别注意链表的头节点和尾节点。
代码实现:
SingleList* Delete(SingleList *head,SingleList *node)
//在单链表中删除一个节点
//输入参数:单链表的头指针和要删除的节点
//输出参数:无
//返回值:指链表的头指针
SingleList* Delete(SingleList *head,SingleList *node)
{
SingleList *pSL;
//链表为空或要删除的结点为空
if((head == NULL)||(node == NULL))
{
return head;
}
//如果删除的不是尾节点
if(node->next != NULL)
{
pSL = node->next;
node->next = node->next->next;
node->data = pSL->data;
delete pSL;
pSL = NULL;
return head;
}
//如果删除的是尾节点
else
{
pSL = head;
while((pSL->next != NULL)&&(pSL->next != node))
{
pSL = pSL->next;
}
if(pSL->next != NULL)
{
pSL->next = pSL->next->next;
delete node;
}
return head;
}
}
3 查找单向链表中的一个节点
思路:
这个问题比较简单,只需要在从头一步一步的逐个判断节点值是不是要找的值就可以了。
代码实现:
ViSingleList* Select(SingleList *head,int data)
//在单链表中查找一个元素
//输入参数:单链表的头指针和要查找的元素值
//输出参数:无
//返回值:指向元素的节点或NULL
SingleList* Select(SingleList *head,int data)
{
while(head)
{
if(head->data == data)
{
return head;
}
head = head->next;
}
return head;
}
扩展问题1:查找单向链表中的倒数第k个节点。
思路:按照遍历查找链表的常用模式,可能会马上想到一种简单的方法就是:先遍历链表,看一下链表中有多少个节点。假设链表中有n个节点,那么要找倒数第k个,也就是正数n-k+1个了。接下来,只要指针从头向后走n-k步就可了。这种方法大约要执行2n-k次。
那么,有没有比上面的方法更高效的方法呢?把思维打开,不要固定在遍历链表的时候只能用有一个指针的思维定式上。我们可以尝试用多个指针以不同的次序开始遍历链表。
对于,这个问题,我们就可以设置两个指针,一个指针先在链表上走K步,然后另一个指针从链表头开始和前一个指针一起向后走,这样当第一个指针到达链表尾部时候,第二个指针就会在第一个指针前面k个结点处。这时就找到了倒数第K个节点,算法执行的次数为n 次,比上一个方法减少了一些步,如图4。
图4 查找倒数第k个节点
代码实现:
gleList* returnNodeFromBack(SingleList* head,int k)
//返回链表中的倒数第K节点的指针
//输入参数:单链表的头指针,要查找的节点的倒数位置
//输出参数:无
//返回值:成功返回节点指针,失败返回NULL
SingleList* returnNodeFromBack(SingleList* head,int k)
{
SingleList *firstPtr,*secondPtr;
int count = 0;
firstPtr = secondPtr = head;
while((firstPtr)&&(count < k))
{
firstPtr = firstPtr->next;
count++;
}
if(count < k)
{
return NULL;
}
while(firstPtr)
{
firstPtr = firstPtr->next;
secondPtr = secondPtr->next;
}
return secondPtr;
}
扩展问题2:查找单向链表中的中间节点,当节点个数为偶数时返回中间两个元素中的前者(后者)
思路:类似于第一个扩展问题,对于查找中间元素,我们首先也可以利用先遍布链表看一看总共有多少个节点,然后再走节点总个数的一半即可找到中间元素。显然,这种方法也是要遍历两次链表。
那么,能不能借鉴上面的改进方法再来改进一下这个问题呢。当然可以,在此问题中依然使用两个遍历指针。让第一个指针每次走两步,第二个指针每次走一步,这样因为第一个指针经过的节点数是第二指针的两倍,所以当第一个指针到达中点时,第二个指针正好处于链表的中间位置。
代码实现:
SingleList* returnMidNode(SingleList* head)
//返回链表的中间节点
//输入参数:单链表的头指针
//输出参数:无
//返回值:中间节点的指针或NULL
SingleList* returnMidNode(SingleList* head)
{
SingleList *firstPtr,*secondPtr;
firstPtr = secondPtr = head;
//链表中没有节点或只有一个节点
if((firstPtr == NULL) || (firstPtr->next == NULL))
{
return firstPtr;
}
//while((firstPtr)&&(firstPtr->next)) //偶数个数时返回中间两个索引较大者
while((firstPtr->next)&&(firstPtr->next->next))//偶数个数时返回中间两个索引较小者
{
firstPtr = firstPtr->next->next;
secondPtr = secondPtr->next;
}
return secondPtr;
}
举一反三,将方法推广,我们也可以很容易地找到链表中前三分之一位置上的数。