链表的概念
链表,顾名思义,是以链状形式存储的一系列数据。链状结构的特点是环环相扣
从一个节点只能走到相邻的下一个节点。链表中的每个节点我们一般用一个结构体来表示,每个节点实际存储位置在内存中是随机的,而不一定是顺序的。我们可以把链表的节点想象成购物商场的储物柜。商场里有很多储物柜,一个储物柜就像一个链表节点:
struct ListNode
{
int data;
} A;
储物柜上的编号就像该节点在内存中实际存储的位置:
&A
现在我开了一个柜子A(编号可能是66,也可能是5201314,who cares):
ListNode A;
拿到一个钥匙(指向结构体的指针):
ListNode pA = &A;
把东西(数据)丢进去:
A.data = 1;
可是东西(数据)太多了,我只好再开几个柜子。
ListNode B,C,D,E;
... //同上
东西(数据)放完了,此时我手上有五把钥匙:
ListNode *pA,*pB,*pC,*pD,*pE;
可是我不想带这么多钥匙。我灵光一闪,依次把下一个柜子的钥匙放在上一个柜子里,于是现在柜子变成了这样:
struct ListNode
{
int data;
ListNode * next;
}
依次放完后:
A.next = pB, B.next = pC, C.next = pD, D.next= pE, E.next = NULL;
我手上只有一把钥匙,也就是A柜的钥匙pA
因为有一些柜子被其他人占用了,所以我的这些柜子的编号并不是连续的。
但是我的这种存放策略就不用关心编号的事情了,甚至我都不需要记住我一共开了多少个柜子。我只需要按照钥匙的顺序来依次开柜子拿东西,一直拿到某个柜子里面没有钥匙了(NULL
):
for(ListNode *p = pA ; p->next!=NULL,; p = p->next )
printf("%d ",p->data);
当我有新的东西要放进去时,只需要再开一个柜子F,然后把F的钥匙放在E中即可:
ListNode F, *pF = &F;
F.data=6;
E.next = pF;
如果我想让柜子F在B,C之间(插入节点),即A->B->F->C->D->E,只需要把C柜的钥匙(指向C的指针)放入F,把F柜的钥匙放入B即可:
F.next = pC;
B.next = pF;
如果我想不使用C柜(删除节点),我只需要把D柜的钥匙放入B:
B.next = D;
以上就是链表的工作原理
双向链表也就是在此基础上每个节点还存有上一个节点的地址,这样可以往两个方向挨个儿访问。
实现
实际在编程当中,我们往往不会像上面那样每个节点都声明,赋值,节点的个数也一般是不确定的。
添加节点到末尾
基于C++的语法特性,我们可以这样建立一个链表并且读入数据:
ListNode * pA = new ListNode(); //开辟一块新的内存空间,类似C中的 malloc
ListNode * p = pA;
for(int i=0;i<n;i++,p=p->next)
{
cin >> p->data;
p->next = new ListNode();
}
把添加节点到链表末尾的功能封装到结构体里面:
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(NULL) {}
void push_back(int data) //在尾部添加元素
{
ListNode* p = this;
while (p->next != NULL) p = p->next;
p->next = new ListNode(data);
}
};
在指定位置插入节点:
两种情况,第一种我们需要插入到指定数据内容的节点后面,比如说我不知道他是第几个节点,但我知道他的data是2(如果有多个data为2的节点,那么会插入到第一个data为2的节点后面):
int insert(ListNode * pB,ListNode * pA,int data) //把B节点插入到A链表中数据为data的节点后面
{
int pos = 0;
while(pA!=NULL && pA->val != data) {pA=pA->next; pos++;} //往后找val为data的节点
if(pA==NULL) return -1; //没有指定的节点,返回-1
else{
if(pA->next != NULL) pB->next=pA->next; //如果该节点不是最后一个节点那么pB->next应该赋值为原本pA后面的节点的地址
else pB->next=NULL; //如果pA指向的是最后一个节点,那么pB指向的节点的next指针应该置为空
pA->next=pB; //pA指向节点的next指针改为指向B节点
return pos; //返回插入节点的索引
}
}
}
第二种我们需要插入到指定的“索引”后面,比如插入到第三个节点后面:
int insert(ListNode * pB,ListNode * pA,int pos) //插入到索引为pos的节点后面
{
for(int i=0;i<=pos;i++,pA=pA->next) //移动到索引为pos的节点
if(pA==NULL) return -1;
if(pA->next!=NULL) pB->next=pA->next; //最后一个节点的情况
else pB->next = NULL //非最后一个节点的情况
pA->next=pB;
return pos;
}
只要把pA改为this指针就可以把整个函数放入结构体中,在此省略,读者可根据需求自行完成
删除指定节点
这个需求和插入节点其实很像了,但是要注意我们操作的是单向链表
,不能倒退,在删除的时候我们应该用pA->next
来判断而不是用pA
来判断是不是指定的节点(想一想,为什么)