链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点组成(链表中每一个元素称为结点),结点可以在运行时动态生成。
每个结点包括两个部分:一个是存储数据元素的数据域(data域),另一个是存储下一个结点地址的指针域(next域)。
链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定。
示意图:

需求:使用带头节点的单向链表,完成对学生对象的增删改查操作。



需求:实现单向链表的增(顺序链尾添加、指定位置添加)删改查、找到单链表倒数第几的节点、获取当前链表大小(即有效数据个数)、链表反转(改变链表结构)、反向打印(不破坏链表结构)、合并两个链表并排序。
public class SingleLinkedListDemo {public static void main(String[] args) {SingleLinkedList singleLinkedList = new SingleLinkedList();System.out.println("---[顺序添加]结果如下:---");singleLinkedList.add(new Student(1, "张三"));singleLinkedList.add(new Student(2, "李四"));singleLinkedList.add(new Student(4, "田七"));SingleLinkedList.foreach(singleLinkedList.getHead());// ---[顺序添加]结果如下:---// no=1, name=张三// no=2, name=李四// no=4, name=田七System.out.println();System.out.println("---[指定位置添加]结果如下:---");singleLinkedList.addByOrder(new Student(3, "(赵六)"));SingleLinkedList.foreach(singleLinkedList.getHead());// ---[指定位置添加]结果如下:---// no=1, name=张三// no=2, name=李四// no=3, name=(赵六)// no=4, name=田七System.out.println();System.out.println("---[链表反转]结果如下:---");SingleLinkedList.reverse(singleLinkedList.getHead());SingleLinkedList.foreach(singleLinkedList.getHead());// ---[链表反转]结果如下:---// no=4, name=田七// no=3, name=(赵六)// no=2, name=李四// no=1, name=张三System.out.println();System.out.println("---[链表反向打印]结果如下:---");SingleLinkedList.reversePrint(singleLinkedList.getHead());// ---[链表反向打印]结果如下:---// no=1, name=张三// no=2, name=李四// no=3, name=(赵六)// no=4, name=田七System.out.println();System.out.println("---[修改节点]结果如下:---");singleLinkedList.update(new Student(3, "(王九)"));SingleLinkedList.foreach(singleLinkedList.getHead());// ---[修改节点]结果如下:---// no=4, name=田七// no=3, name=(王九)// no=2, name=李四// no=1, name=张三System.out.println();System.out.println("---[删除节点]结果如下:---");singleLinkedList.delete(3);SingleLinkedList.foreach(singleLinkedList.getHead());// ---[删除节点]结果如下:---// no=4, name=田七// no=2, name=李四// no=1, name=张三System.out.println();System.out.println("---获取[当前链表中有效数据的个数]结果如下:---");System.out.println(SingleLinkedList.size(singleLinkedList.getHead()));// ---获取[当前链表中有效数据的个数]结果如下:---// 3System.out.println();System.out.println("---获取[当前链表中倒数第二个数据]结果如下:---");Student reciprocalNode = SingleLinkedList.findReciprocalNode(singleLinkedList.getHead(), 2);System.out.println(reciprocalNode);// ---获取[当前链表中倒数第二个数据]结果如下:---// no=2, name=李四}/*** 测试两个链表合并后排序。*/@Testpublic void testMerge() {SingleLinkedList s1 = new SingleLinkedList();s1.add(new Student(1, "a"));s1.add(new Student(3, "c"));s1.add(new Student(5, "e"));SingleLinkedList s2 = new SingleLinkedList();s2.add(new Student(2, "b"));s2.add(new Student(4, "d"));Student mergeHead = SingleLinkedList.mergeAndSorted(s1.getHead(), s2.getHead());assert mergeHead != null;System.out.println("---[链表合并排序后]结果如下:---");SingleLinkedList.foreach(mergeHead);// ---[链表合并排序后]结果如下:---// no=1, name=a// no=2, name=b// no=3, name=c// no=4, name=d// no=5, name=e}
}/*** 学生节点。*/
class Student {public int no;public String name;/*** 指向后一节点。*/public Student next;public Student(int no, String name) {this.no = no;this.name = name;}@Overridepublic String toString() {return "no=" + no +", name=" + name;}
}/*** 单向链表实现。*/
class SingleLinkedList {/*** 头节点(勿动,仅占位)。*/private final Student head = new Student(0, "");public Student getHead() {return head;}/*** 断言链表是否为空。** @param node 节点*/private static void assertLinkedListIsNull(Student node) {if (null == node || null == node.next) {throw new NullPointerException("链表为空!");}}/*** 迭代器。** @param head 头节点*/public static void foreach(Student head) {assertLinkedListIsNull(head);// 由于头节点不能动,所以这里要添加辅助指针来帮助进行遍历。Student pointer = head;while (null != pointer.next) {System.out.println(pointer.next);// 后移。pointer = pointer.next;}}/*** 链尾添加。** @param node 节点*/public void add(Student node) {Student pointer = this.head;while (null != pointer.next) {pointer = pointer.next;}// 循环终止时,表示到了链尾。pointer.next = node;}/*** 根据编号指定位置进行添加。** @param node 节点*/public void addByOrder(Student node) {Student pointer = this.head;boolean exist = false;while (null != pointer.next) {// 1.找到了插入位置。if (pointer.next.no > node.no) {break;// 2.节点编号已存在。} else if (pointer.next.no == node.no) {exist = true;break;}// 后移。pointer = pointer.next;}// 判断循环终止的条件。if (exist) {System.out.printf("编号[%d]的节点已存在,添加失败!\n", node.no);} else {// 插入。node.next = pointer.next;pointer.next = node;}}/*** 更新节点。** @param node 节点*/public void update(Student node) {Student pointer = this.head;boolean found = false;assertLinkedListIsNull(pointer);while (null != pointer.next) {// 找到了需要修改的节点。if (pointer.next.no == node.no) {found = true;break;}pointer = pointer.next;}if (found) {pointer.next.name = node.name;} else {System.out.printf("编号[%d]的节点不存在,修改失败。\n", node.no);}}/*** 删除节点。** @param no 编号*/public void delete(int no) {Student pointer = this.head;boolean found = false;assertLinkedListIsNull(pointer);while (null != pointer.next) {if (pointer.next.no == no) {found = true;break;}pointer = pointer.next;}if (found) {// 指向下下个节点,被跳过节点将失去引用被垃圾回收机制进行回收。pointer.next = pointer.next.next;} else {System.out.printf("编号[%d]的节点不存在,删除失败。\n", no);}}/*** 链表大小(即有效数据个数)。** @param head 节点* @return int*/public static int size(Student head) {Student pointer = head;if (null == pointer.next) {return 0;}int size = 0;while (null != pointer.next) {size++;pointer = pointer.next;}return size;}/*** 找到倒数第几的节点。** @param head 头节点* @param index 倒数第几* @return {@link Student}*/public static Student findReciprocalNode(Student head, int index) {assertLinkedListIsNull(head);int size = size(head);if (0 >= index || index > size) {return null;}Student pointer = head;// 假设3个数据,需要倒数第2个,3-2=1。则循环1次。for (int i = 0; i < (size - index); i++) {pointer = pointer.next;}return pointer.next;}/*** 链表反转(改变链表结构)。** @param head 头节点*/public static void reverse(Student head) {// 链表中没有节点或者只有一个节点时,无需反转。if (null == head.next || null == head.next.next) {return;}// 辅助指针。Student pointer = head.next;// 临时变量,用于存储当前节点的下一节点。Student temp;// 新的链表头。Student reverseHead = new Student(0, "");while (null != pointer) {temp = pointer.next;// 顺序取出后进行赋值。pointer.next = reverseHead.next;reverseHead.next = pointer;// 后移。pointer = temp;}// 节点指向,完成反转。head.next = reverseHead.next;}/*** 反向打印(不破坏链表结构)。** @param head 头*/public static void reversePrint(Student head) {assertLinkedListIsNull(head);Student pointer = head.next;Stack stack = new Stack<>();// 压栈。while (null != pointer) {stack.push(pointer);pointer = pointer.next;}// 弹栈。while (!stack.isEmpty()) {System.out.println(stack.pop());}}/*** 合并两个链表并排序。** @param head1 head1* @param head2 head2* @return {@link Student}*/public static Student mergeAndSorted(Student head1, Student head2) {if (null == head1.next && null == head2.next) {return null;}// 较小头节点。Student head = head1.no <= head2.no ? head1 : head2;// 较小头节点的下一节点。Student cur1 = head.next;// 较大头节点。Student cur2 = (head == head1) ? head2 : head1;// 辅助指针Student pointer = head;while (null != cur1 && null != cur2) {// 较小值插入链表。if (cur1.no <= cur2.no) {pointer.next = cur1;cur1 = cur1.next;} else {pointer.next = cur2;cur2 = cur2.next;}// 后移。pointer = pointer.next;}pointer.next = (null != cur1) ? cur1 : cur2;return head.next;}
}
与单向链表不同的是,双向链表增加了指向前一节点的属性。
两种链表区别:
单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除。所以前面我们单链表删除
时节点,总是需要找到 pointer 的下一个节点。
双向链表增删改查实现思路分析:
示意图:

需求:实现双向链表的增(顺序链尾添加、指定位置添加)删改查。
public class DoubleLinkedListDemo {public static void main(String[] args) {DoubleLinkedList doubleLinkedList = new DoubleLinkedList();System.out.println("---[顺序添加]结果如下:---");doubleLinkedList.add(new Teacher(1, "张老师"));doubleLinkedList.add(new Teacher(2, "李老师"));doubleLinkedList.add(new Teacher(4, "田老师"));doubleLinkedList.foreach();// ---[顺序添加]结果如下:---// no=1, name=张老师// no=2, name=李老师// no=4, name=田老师System.out.println();System.out.println("---[指定位置添加]结果如下:---");doubleLinkedList.addByOrder(new Teacher(3, "(赵老师)"));doubleLinkedList.foreach();// ---[指定位置添加]结果如下:---// DEBUG INFO (addByOrder result) : prev.no=[2] ,cur.no=[3] ,next.no=[4]// no=1, name=张老师// no=2, name=李老师// no=3, name=(赵老师)// no=4, name=田老师System.out.println();System.out.println("---[修改节点]结果如下:---");doubleLinkedList.update(new Teacher(3, "(王老师)"));doubleLinkedList.foreach();// ---[修改节点]结果如下:---// no=1, name=张老师// no=2, name=李老师// no=3, name=(王老师)// no=4, name=田老师System.out.println();System.out.println("---[删除节点]结果如下:---");doubleLinkedList.delete(3);doubleLinkedList.foreach();// ---[删除节点]结果如下:---// DEBUG INFO (delete before) : prev.no=[2] ,cur.no=[3] ,next.no=[4]// no=1, name=张老师// no=2, name=李老师// no=4, name=田老师}
}/*** 双向链表实现。*/
class DoubleLinkedList {/*** 头节点勿动。*/private final Teacher head = new Teacher(0, "");public Teacher getHead() {return head;}/*** 链尾添加。** @param node 节点*/public void add(Teacher node) {Teacher pointer = this.head;while (null != pointer.next) {pointer = pointer.next;}pointer.next = node;node.prev = pointer;}/*** 根据编号指定位置进行添加。** @param node 节点*/public void addByOrder(Teacher node) {Teacher pointer = this.head;boolean exist = false;while (null != pointer.next) {if (pointer.next.no > node.no) {break;} else if (pointer.next.no == node.no) {exist = true;break;}pointer = pointer.next;}if (exist) {System.out.printf("编号[%d]的节点已存在,添加失败!\n", node.no);} else {// 后一节点双链指向。no.3 <-> no.2// 注意这里后一节点可能为空if (null != pointer.next) {node.next = pointer.next;node.next.prev = node;}// 前一节点双链指向。no.1 <-> no.2pointer.next = node;node.prev = pointer;// 结果验证。System.out.printf("DEBUG INFO (addByOrder result) : prev.no=[%d] ,cur.no=[%d] ,next.no=[%d]\n",node.prev.no,node.no,(null == node.next ? null : node.next.no));}}/*** 更新节点。** @param node 节点*/public void update(Teacher node) {Teacher pointer = this.head;assertLinkedListIsNull(pointer);boolean found = false;while (null != pointer.next) {if (pointer.next.no == node.no) {found = true;break;}pointer = pointer.next;}if (found) {// 只是当前节点的值变了,前后链接指向关系没有动。pointer.next.name = node.name;} else {System.out.printf("编号[%d]的节点不存在,修改失败。\n", node.no);}}/*** 删除节点。** @param no 编号*/public void delete(int no) {Teacher pointer = this.head.next;assertLinkedListIsNull(pointer);boolean found = false;while (null != pointer) {if (pointer.no == no) {found = true;break;}pointer = pointer.next;}if (found) {// before: no.2(pointer.prev) <-> no.3(pointer) <-> no.4(pointer.next)// after: no.2 <-> no.4System.out.printf("DEBUG INFO (delete before) : prev.no=[%d] ,cur.no=[%d] ,next.no=[%d]\n",pointer.prev.no,pointer.no,pointer.next.no);// 后链被删除的后一节点。pointer.prev.next = pointer.next;// 注意:如果是最后一个节点,就不需要执行下面这句话,否则出现空指针!if (pointer.next != null) {// 被删除的后一节点往前链。pointer.next.prev = pointer.prev;}} else {System.out.printf("编号[%d]的节点不存在,删除失败。\n", no);}}/*** 断言链表是否为空。** @param node 节点*/private static void assertLinkedListIsNull(Teacher node) {if (null == node || null == node.next) {throw new NullPointerException("链表为空!");}}/*** 迭代器。*/public void foreach() {Teacher pointer = this.head;assertLinkedListIsNull(pointer);while (null != pointer.next) {System.out.println(pointer.next);pointer = pointer.next;}}
}/*** 教师节点。*/
class Teacher {public int no;public String name;/*** 指向前一节点。*/public Teacher prev;/*** 指向后一节点。*/public Teacher next;public Teacher(int no, String name) {this.no = no;this.name = name;}@Overridepublic String toString() {return "no=" + no +", name=" + name;}
}
约瑟夫问题,是一个计算机科学和数学中的问题,在计算机编程的算法中,类似问题又称为约瑟夫环,又称“丢手绢问题”。
据说著名犹太历史学家 Josephus 有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与 Josephus 及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而 Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus 要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
用一个不带头结点的循环链表来处理 Josephus 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。
构建单向环形链表:
遍历单向环形链表:
示意图:

public class Josephus {public static void main(String[] args) {CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();System.out.println("[添加节点]结果如下:");circleSingleLinkedList.add(5);circleSingleLinkedList.foreach();// [添加节点]结果如下:// no=1// no=2// no=3// no=4// no=5System.out.println();System.out.println("[出圈]结果如下:");// 圈中总计节点个数5,从1开始计数,每次数两下,被点到的编号出圈。// 期望结果:2 -> 4 -> 1 -> 5 -> 3circleSingleLinkedList.count(1, 2, 5);// [出圈]结果如下:// 编号[2]出圈// 编号[4]出圈// 编号[1]出圈// 编号[5]出圈// 圈中剩余编号为[3]}
}class CircleSingleLinkedList {/*** 首节点。*/private Node first = null;/*** 添加节点。** @param nums 生成的节点个数*/public void add(int nums) {// 值校验,添加的点需要大于1。if (1 > nums) {throw new IllegalArgumentException();}// 辅助指针。Node pointer = null;for (int i = 1; i <= nums; i++) {Node node = new Node(i, null);if (1 == i) {// 构造环(首尾相连)。first = node;first.setNext(first);// 并指向第一个节点。pointer = first;} else {// 节点往后添加。if (null != pointer) {pointer.setNext(node);// 闭环。node.setNext(first);pointer = node;}}}}/*** 迭代器。*/public void foreach() {if (null == this.first || null == this.first.getNext()) {throw new NullPointerException();}Node pointer = this.first;while (true) {System.out.println(pointer);// 一圈结束。if (first == pointer.getNext()) {break;}pointer = pointer.getNext();}}/*** 根据用户的输入,计算出圈的顺序。** @param start 从第几开始* @param nums 数几次* @param total 最初总共有多少个*/public void count(int start, int nums, int total) {if (null == this.first || 1 > start || start > total) {throw new IllegalArgumentException();}Node pointer = this.first;while (first != pointer.getNext()) {pointer = pointer.getNext();}// 先让首节点和辅助指针后移 start - 1 次。for (int i = 0; i < (start - 1); i++) {first = first.getNext();pointer = pointer.getNext();}// 圈中只有一个节点时退出。while (first != pointer) {// 让首节点和辅助指针后移 nums - 1 次。for (int i = 0; i < (nums - 1); i++) {first = first.getNext();pointer = pointer.getNext();}System.out.printf("编号[%d]出圈\n", first.getNo());first = first.getNext();pointer.setNext(first);}System.out.printf("圈中剩余编号为[%d] \n", first.getNo());}
}class Node {private int no;private Node next;public Node(int no, Node next) {this.no = no;this.next = next;}public int getNo() {return no;}public void setNo(int no) {this.no = no;}public Node getNext() {return next;}public void setNext(Node next) {this.next = next;}@Overridepublic String toString() {return "no=" + no;}
}
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。