数据结构---单链表
难得有空闲的时间,刚好近段时间在学习算法导论,索性便理一理自己对数据结构的理解.相信大部分初学者都是学习严奶奶版的数据结构,因此,本文便使用该版数据结构中的例子来解释下单链表的插入和删除.管中窥豹,可见一斑,个人看法有限,可供侧面参考.
开门见山,先给出几个定义:
链表结点结构:
Typedef Struct ListNode
{
ElemType m_nKey;
ListNode* m_pNext;
}*LinkList;
几个术语:
头指针:顾名思义,其只是一个指针,代表指向头结点的指针.
头结点:头结点一般只有指针域,头结点是处于第一元素节点之前的节点,第一个结点就是第一个含有数据元素的节点.
首元结点: 第一个含有数据元素的节点.
三者的图示如下:
链表的插入:
首先,先看下插入过程的图示:
从图中,我们可以看出插入一个节点需要经过如下三个步骤:
(1) 找到指向第三个结点的指针,也即找到第二结点.
(2) 让新节点指针域指向原先的第三个结点.
(3) 让第二个结点的指针域指向新的节点.
第一步比较复杂点,其余两步较为简单.
(1)找到指向指定结点的指针(即找到指定结点的前一个结点).
如果一个链表没有设计尾指针,则该链表唯一的已知条件或者说可用条件只能是头指针,要得到链表中的任意一个节点,都只能从头指针所指向的头结点开始遍历.
说到遍历, 遍历的过程中涉及了很多循环,而在含有循环的代码中经常出现的问题就是在循环结束条件的判断,是该用小于还是小于等于?是该用i还是i-1?对于这方面问题,没有捷径可走,在编写完代码之后,再用边界值、边界值减1、边界值加1等都尝试检查并运行一次.所以,对于第一个步骤来说,难点主要在于遍历过程中循环的控制.
那么,首先考虑下遍历的逆推过程,要找到指向指定节点的指针→该指针保存在其前一个结点的指针域中→要找到指向前一个结点的指针→该指针保存在其前一个结点的指针域中→…..→找到头指针.因此,当定义一个遍历过程中的指针ListNode* pCur时,毫无疑问,初始值就为head指针(头指针),上面逆推的终点,即ListNode*pCur=head;. 除此之外,还需要一个int型的变量j来记录当前位置,那么又需要考虑一个问题,初始位置的值该设为多少呢?其实,在链表中,在计算节点总数时,头结点并没有参与计数.所以,我们可以认为当前头结点的位置为0;这样一来,当前指针pCur指向头结点,位置为0.当改变pCur指向时,对j加一,这是就会出现pCur指向第一个结点,j=1;两者始终保持一致.可以通过下面的例子来理解.
条件是头指针Head,要查找结点的位置为i,那么可以设计出如下的遍历方式:
ListNode* TraverList(LinkList head,int i)
{
ListNode*pCur=head;
int j=0;
while(pCur->m_Next!=NULL && j<i)
{
pCur=pCur->pNext;
j++;
}
Return pCur;
}
当还未开始循环时,pCur是头指针,而这时候位置值j为0,那么我们就把头结点认为是第0个结点,因为这样的话,指针和位置值就能够始终保持一致,也是一个循环不定式,看下循环过程(假设结点总数为10,i=5):
第一次循环结束:pCur指向第一个结点,j=1 当前确实为第一个结点;
第二次循环结束:pCur指向第二个结点,j=2 当前确实为第二个结点;
第三次循环结束:pCur指向第三个结点,j=3 当前确实为第三个结点;
第四次循环结束:pCur指向第四个结点,j=4 当前确实为第四个结点;
第五次循环结束:pCur指向第五个结点,j=5 当前确实为第五个结点;
第六次…………:因为j==i了.所以无法进入循环,就没有在改变pCur的指向了,即指向第五个结点,可见是正确的。
首先, pCur->m_Next !=NULL只是为了能够在从第一个结点遍历到最后一个结点所设置的条件,因为最后一个结点的指针域为空.
其次,为什么使用j<i而不是j<=i呢?其实,从上面可以看出来,因为j的初值为0,所以使用j<i,刚好可以执行i次循环,而不是i-1次循环,而因为i的值始终和pCur保持保持一致,因此,可以保证每次pCur的指向刚好是第i个结点.或许从另一个角度来看, 那么为什么不是j<=i呢?假设我们i=3,那也就是说要找到第三个结点,当第二次循环结束时,j=2,这时还能进入循环,一旦进入循环,也就意味着pHead就从原来的第二个结点指向第三个结点,而j也等于3,这时候就不需要在进入循环了,因为我们需要的结果已经得到.如果这时j<=3,就能再次进入循环,就导致指向了第四个结点,就不是我们想要的结果了。(也就是说,当我们想要第N个几点时,我们要保证能够刚好进入第N次循环(初始值为0),那么也是这次循环将原本的pCur指向第N-1结点设为第N个结点)
最后,初始值j设置为0,只是为了可以和pCur保持一致,因为pCur的初始值是指向头结点,头结点并不能算入元素节点,因此刚好以0来计数.
(2) 让新结点指针域指向指定结点.
设新结点为: ListNodes=malloc(sizeof(ListNode))
s->m_pNext=pCur->m_pNext;
(3) 让指定结点的前一个结点的指针域指向新节点.
pCur->m_pNext=s;
可以写出最终的插入函数为:
StatusListInsert_L(LinkList* pListHead,int i,ElemType e)
{
if(pListHead==NULL)
{
return error;
}
//定义开始遍历的指针和要插入位置的前一个位置j
ListNode* pCur=pListHead;
Int j=0;
//寻找i结点的前一个结点,即第i-1个结点(也即表示找到指向该结点的指针)
While(pCur->m_pNext !=NULL &&j<i-1)
{
pCur=pCur->m_pNext;
j++;
}
//判断是否成功
If(!pCur || j>i-1)
{
Return error;
}
//步骤二和步骤三
If((ListNodes=malloc(sizeof(ListNode)))!=NULL)
{
s->m_nKey=e;
s->m_pNext=pCur->m_pNext; 步骤二
pCur->m_pNext=s; 步骤三
return OK;
}
}
链表的删除:
链表的删除原理和插入大同小异,不在赘述.其最终代码如下:
Statue ListDelete_L(ListNode* pListHead,int I,ElemType &e)
{
if(pListHead==NULL)
{
return error;
}
ListNode* pCur=pListHead;
Int j=0;
//寻找i结点的前驱结点,即第i-1个结点(也即表示找到指向该结点的指针)
While(pCur->m_pNext !=NULL &&j<i-1)
{
pCur=pCur->m_pNext;
j++;
}
ListNode temp=pCur->m_pNext;
e=pCur->m_nKeys;
pCur->m_pNext=temp->m_pNext;
free(temp);
return OK;
}