数据结构与算法
链表LinkedList
链表是有序列表,但其在内存中并非有序存储,如单链表:

以节点的方式,链式存储。按实际需求,也有无头节点的链表。
1. 单链表SingleLinkedList
单链表逻辑结构:

- 每个节点都包含两个域
- data域,用来存储数据对象
- next域,指向下一个节点
- head头节点,不存放具体数据,只表示单链表的头,不能移动
- 尾节点,即链表最后的一个节点,next域为null

分析
首先会创建一个头结点,
data域为空,next域为空得出结论,当head头节点的next域为空时,单链表为空
向链表中加入一个节点:
- 找到尾节点
- 使尾节点的
next域指向此节点
尾节点的next域始终为空
- 所以,除了head节点以外,当节点next域为空时,此节点为尾节点
联想
考虑节点排序时,向链表加入一个节点:
找到编号大于新加入节点的节点,即满足
linkedNode.id>newNode.id的节点执行插入,将此节点放在新节点的
next域,然后把新节点放入上一节点的next域,即newNode.next=linkedNode;
linkedNode.pre.next=newNode;
//而实际上linkedNode.pre是不存在的,因为单链表的节点只有next域,不能用当前节点表示上一个节点
//设linkedNode的上一个节点为preNode==linkedNode.pre
//则可以把linkedNode表示为pretNode.next,所以上面2个语句为:
newNode.next=preNode.next;
preNode.next=newNode;
删除一个节点
找到编号等于指定编号的节点,即满足
linkedNode.id==deleteId的节点执行删除,用此节点的后一节点,顶替此节点的位置,即
linkedNode.pre.next=linkedNode.next;
//同理,设linkedNode的上一个节点为preNode==linkedNode.pre,则有:
preNode.next=preNode.next.next;
//此时在链表外的linkedNode成了不被引用的对象,会被JVM垃圾回收机制自动回收
- 修改节点信息
- 根据
id找到对应节点,修改data域信息即可。
- 根据
- 统计单链表内有效节点的个数
size- 让
temp从第一个有效节点head.next开始遍历,计数即可。
- 让
- 查找到单链表中倒数第
n个节点- 需要先获得链表有效节点的个数
size - 第
(size-n+1)个节点,便是倒数第n个节点
- 需要先获得链表有效节点的个数
- 反转单链表,即:假如有节点为1234的链表,将它反转为4321
- 创建一个新的头结点newHead,用来作为新链表的头
- 遍历原链表时,将当前链表的节点放入newHead与newHead后一个节点之间
- 遍历原链表时,需要保存当前节点的下一个节点,以便返回到原链表
- 最后遍历完成后,将新节点的第一个有效节点,放入原head节点的next域
- 逆序打印单链表
- 遍历时,把单链表中节点压入栈中,然后再从栈中取出的节点并打印即可。
- 合并两个有序的单链表,使之仍然有序(示例中为由小到大)
- 初始化temp1为链表1的第一个有效节点。而temp2为链表2的头结点
- 将链表1放入链表2中,使temp2不断后移。在找到temp1的插入位置后,temp1才后移。
- 若直到temp2后移到末尾都没有找到temp1能插入的位置,则把temp1置于temp2末尾,结束循环。
- 取出链表中所有节点的集合
- 遍历取出,使用List集合
- 注意,取出时要把每个节点的next域置为空,注意临时保存断掉的链表,以及循环退出条件
- 按顺序批量添加节点
- foreach遍历上面方法取出的节点集合,然后调用排序添加节点的方法即可。
- 使一个无序链表转换为有序列表(示例中为由小到大)
- 与上面的方法结合,就可以实现2个无序列表合并到1个有序列表
单链表java实现
Node节点类
1 | package LinkedList; |
单链表类
1 | package LinkedList; |
输出结果:
1 | **********无序链表1 |
2. 双向链表DoublyLinkedList
双向链表逻辑结构:

参照单向链表,双向链表在单向链表的基础上,在每个节点多了一个pre域,指向上一个节点。
- 双向链表中的节点可以获取父节点
- 增删时需要把next域和pre域都修改
- 按顺序增加时,即插入操作:
- 修改pre和next时,注意执行顺序
- 先让
node的pre和next分别连到temp和temp.next - 然后将
temp.next的pre连到node - 最后才将
temp的next连到node,否则会找不到原先的temp.next
- 由于过渡节点temp可以指向上一个节点,方便了操作,所以删除时temp可以初始为第一个有效节点:
- 此时
temp为要删除的节点,temp.pre和temp.next的中间是temp。 - 将
temp.next的pre连到temp.pre,把temp.pre的next连到temp.next。 - 还要注意,当要删除的节点为尾节点时,
temp.next.set()会造成空指针异常
- 此时
我们之前已经详细的分析了单向链表,其余请对照单向链表思考,修改代码。
双向链表java实现
1 | package LinkedList; |
3. 环形链表
单向环形链表
图示:

这是最简单的形式,除了没有头结点和尾节点外,也没有其他环形结构
- 单个节点的next指向自己,也可以成为环形
- 遍历方法:
- 设一个指针指向第一个节点,命名为
first,该指针位置不变。 - 设一个移动的指针,初始为
first,命名为temp,显然,当temp.next==first时,遍历结束,此时temp表示链表尾部(逻辑上) - 遍历前,应当确保temp==first,才能从头遍历
- 加入新节点时,将链表遍历到尾节点,然后将尾节点的next指向新节点,再把新节点的next指向first。
- 设一个指针指向第一个节点,命名为
java实现
1 | package LinkedList; |
约瑟夫置换
单向链表的实际应用中,约瑟夫问题是非常经典的。下面使用单向环形链表易懂的解决约瑟夫问题,及其变体。
百度百科——约瑟夫问题


分析
- 这里我们依然使用
Node节点类,不再新建。 - 需要把
N个节点加入链表,并且自动编号 - 从
1位置开始计数,temp每后移1次,计数变量i的值就+1 - 删除条件:当计数变量
i==m时,删除当前temp指向的节点- 要注意,要想删除
temp,就需要找到temp.pre,但是单向链表的节点只有next - 在
i==m-1时,temp.next就是i==m时的要删除的temp。所以只需要在i==m-1时把temp.next删除即可
- 要注意,要想删除
- 遍历结束条件:链表中只剩一个节点时,即
temp.next==tem
java实现
1 | //根据约瑟夫问题删除节点 |
运行结果
1 | 3号节点出列 |
约瑟夫问题变体
约瑟夫问题变体很多,解决方式更是多种多样。都在约瑟夫问题的基础上变动,如:
设编号为1,2,··· n的n个人围坐一圈,设编号为k的人从1开始报数,数到m的那个人出列,然后从出列的下一位开始重新从1报数,如此循环,直到只有最后一个人为止,求出队的人的编号序列
相对于原生的约瑟夫问题,这里不在是从1号位置开始报数,而是改成了从k号位置报数,k成了 一个第三方指定的变量。
思路
由于与上个问题区别不大,我们这次换一种不同的思路来解决。
- 注意判断
k是否在符合规则的范围内 - 报数前,将
first指向k号节点,将temp指向此时first的前一个节点 - 报数时,将
first和temp同时移动m-1次,每次移动1位。完成后,此时first就指向需要删除的节点m,temp指向first指向节点的前一个节点 - 把
first.next设为新的first节点,然后把first指向temp.next,如此循环即可