赞
踩
什么是链表?
链表(Linked List)是用链式存储结构实现的线性表。链表示意图:
链表的组成:数据域
+引用域
(数据域和引用域合称结点或元素)
链表的特点:
链表中元素的联系依靠引用域
具有线性结构的特点,链表所使用的逻辑结构是线性结构
具有链式存储结构的特点,所使用的物理存储结构是链式存储
链表的分类:
单向链表:单链表是一种最简的链表,只有一个引用域1next
特点:通过next可以访问到后继结点,终端结点的引用域指向null
双向链表:具有两个引用域prev和next,prev用来保存前驱结点的地址,next用来保存后继结点的地址
特点:通过next可以访问后继结点,终端结点的next指向null;通过prev可以访问到前驱节点,起始结点的prev指向null
循环链表:循环链表本质是一种特殊的单向链表,只是它的终端结点指向了开始结点(也就是next存放了开始结点的地址)
特点:所有结点都能具有前驱节点和后继结点
链表的使用场景:对查找速度要求不高,但对插入和删除速度要求高时,可以使用链表。常见的比如:
单向链表(简称单链表)有带头结点的单链表,也有不带头链表的单链表。
单链表的基本操作:
带头结点就是先固定一个头节点,用来标识链表的初始位置,它的data域不存任何东西,它的next域用来第一个结点的地址,每次遍历链表或定位结点都需要借助一个辅助变量temp来实现。
插入结点示意图:
删除结点示意图:
修改结点示意图:
遍历经验总结:当我们想要进行的操作的结点依赖于前一个结点时,比如插入、删除、修改等操作操作,就必须从head结点开始遍历,否则会出现空指针异常;当我们想要进行的操作不依赖前一个结点时,就无须从head结点开始遍历,比如根据id获取结点,非空判断、获取链表长度、展示链表等操作。
package com.hhxy.linkedlist; import java.util.Scanner; import com.hhxy.queue.ArrayQueue2; /** * 单向链表测试类 * @author ghp * 测试数据: * 1 宋江 及时雨 * 2 林冲 豹子头 * 3 鲁智深 花和尚 * 4 吴用 智多星 */ public class SingleLinkedListTest { public static void main(String[] args) { Scanner sc = new Scanner(System.in); SingleLinkedListDemo1 sll = new SingleLinkedListDemo1();//创建链表 OUT: while(true) { System.out.println("-------------------单向链表操作界面-----------------"); System.out.println("请输入操作指令:"); System.out.println("0 : 退出程序"); System.out.println("1 : 在链尾添加结点"); System.out.println("2 : 按id从小到大的顺序添加结点"); System.out.println("3 : 根据id获取结点"); System.out.println("4 : 根据id删除结点"); System.out.println("5 : 获取链表中元素的个数"); System.out.println("6 : 展示链表中所有的元素"); System.out.println("7 : 根据id修改结点"); System.out.println("8 : 清空链表"); //用于接收用户输入 int id; String name=""; String alias=""; Student student = null; switch(sc.next()) { case "0": //退出程序 System.out.println("正在退出程序~~~"); break OUT; case "1": //在链尾添加结点 System.out.println("请按照 id name alias 的格式输入要添加的元素:"); id = sc.nextInt(); name = sc.next(); alias = sc.next(); student = new Student(id,name,alias); if(sll.add(student)) System.out.println("结点:"+student+"添加成功!"); break; case "2"://按id从小到大的顺序添加结点 System.out.println("请按照 id name alias 的格式输入要添加的元素:"); id = sc.nextInt(); name = sc.next(); alias = sc.next(); student = new Student(id,name,alias); if(sll.addById(student)) System.out.println("结点:"+student+"添加成功!"); break; case "3"://根据id获取结点 System.out.println("请输入要获取结点的id号:"); id = sc.nextInt(); try { student = sll.get(id); System.out.println(id+"号结点为:"+student); }catch(Exception e){ System.out.println(e.getMessage()); } break; case "4"://根据id删除结点 System.out.println("请输入要删除结点的id号:"); id = sc.nextInt(); try { if(sll.remove(id)) System.out.println("结点删除成功!"); }catch(Exception e) { System.out.println(e.getMessage()); } break; case "5"://获取链表中结点的个数(不包括头节点) System.out.println("链表中的结点个数为:"+sll.size()); break; case "6"://展示链表中所有的结点(不包括头节点) sll.show(); break; case "7"://根据id修改结点 System.out.println("请按照 id name alias 的格式输入要修改的元素:"); student = new Student(sc.nextInt(),sc.next(),sc.next()); try { if(sll.update(student)) System.out.println("修改成功"); }catch(Exception e) { System.out.println(e.getMessage()); } break; case "8"://清空链表 if(sll.clear()) System.out.println("链表已成功清空!"); break; default: System.out.println("请输入有效指令!"); break; } } System.out.println("程序已退出"); } }
package com.hhxy.linkedlist; //结点类 class Student{ //数据域(将成员变量设置为public方便外部访问) public int id; public String name; public String alias; //引用域 public Student next; public Student(int id, String name, String alias) { this.id = id; this.name = name; this.alias = alias; } @Override public String toString() { return "[id=" + id + ", name=" + name + ", alias=" + alias + "]"; } } //链表类 public class SingleLinkedListDemo1 { //初始化头结点 Student head = new Student(-99,"",""); /** * 判断链表是否为空 * @return true表示链表为空 */ public boolean isEmpty() { //因为头节点是链表位置的标识,不能动,所以使用一个辅助引用来遍历链表 Student temp = head.next; if(temp!=null) { //head后面存在至少一个元素,所以链表不为空 return false; } return true; } /** * 在链尾添加结点 * @param student 待添加的结点 * @return true表示添加成功 */ public boolean add(Student student) { //同理,因为链表头节点不能动。 //注意:需要是从头节点开始遍历,因为链表可能为空,如果从头节点后一个遍历,当链表为空时会报空指针异常 Student temp = head; //遍历寻找尾结点。因为temp=head,所以是从头节点开始遍历 while(temp.next != null) { temp = temp.next; } //已找到链表尾结点,进行指向 temp.next = student; return true; } /** * 按照id从小到大的顺序添加结点 * @param student 待添加的结点 * @return true表示添加成功 */ public boolean addById(Student student) { Student temp = head; boolean flag = true;//用于判断链表是加在尾结点,还是加在结点之间 while(temp.next != null) { if(student.id < temp.next.id) { //说明是添加在结点之间 flag = false; break; } temp = temp.next; } if(flag) { //如果添加的结点是在尾结点 temp.next = student; }else { //添加的结点是在结点之间 student.next = temp.next;//切记:先改变后一个指向,再改变前一个指向 temp.next = student; } return true; } /** * 根据id获取结点 * @param id * @return 返回对应id的结点 */ public Student get(int id) { if(isEmpty()) { throw new RuntimeException("该链表为空!"); } Student temp = head.next; //从head结点后面开始遍历 boolean flag = false;//判断链表中是否存在待获取的结点 while(temp != null) { if(temp.id == id) { //找到id对应的结点 flag = true; break; } temp = temp.next; } if(flag) { //如果找到id对应结点 return temp; }else { //如果没有找到id对应的结点 throw new RuntimeException("待获取的结点不存在!"); } } /** * 根据id删除结点 * @param id 待删除结点的id * @return true表示删除成功 */ public boolean remove(int id) { if(isEmpty()) { throw new RuntimeException("链表为空!"); } Student temp = head;//删除结点需要依赖前一个结点,所以从头节点开始遍历 boolean flag = false;//判断链表中是否存在待删除的结点 while(temp.next != null) { if(temp.next.id == id) { //找到该结点 flag = true; break; } temp = temp.next; } if(flag) { //如果找到了要删除的结点 temp.next = temp.next.next; }else { //如果没有找到id对应的结点 throw new RuntimeException("待删除的结点不存在!"); } return true; } /** * 获取链表中结点的个数(不包括头节点) */ public int size() { Student temp = head; int count = 0; //这里虽然遍历了头节点,但是没有遍历尾结点 while(temp.next != null) { count++; temp = temp.next; } return count; } /** * 展示链表中所有的结点(不包括头节点) */ public void show() { if(isEmpty()) { System.out.println("链表为空!"); return; } //注意:不需要展示头节点! Student temp = head; while(temp.next != null) { System.out.println(temp.next); temp = temp.next; } } /** * 根据id修改结点 * @param student 待修改的结点 * @return true表示修改成功 */ public boolean update(Student student) { if(isEmpty()) { throw new RuntimeException("链表为空!"); } Student temp = head; boolean flag = false;//判断链表是否修改成功 while(temp.next != null) { if(temp.next.id == student.id) { //找到要修改的链表 flag = true; student.next = temp.next.next; temp.next = student; break; } temp = temp.next; } if(flag) { //如果修改成功 return true; }else { //如果链表中没有找到待删除的结点 throw new RuntimeException("链表中不存在该结点"); } } /** * 清空链表 * @return true表示清空成功 */ public boolean clear() { /*方式一:直接将头节点指向空(节约时间,但占内存) * head.next = null; * 除头节点以外其它结点存在引用,JVM不会回收结点内存,仍然会占据内存 * 这种清楚方法很费内存! */ //方式二:将所有结点占据的内存都进行释放(耗时,但不占内存) Student2 temp = head;//这里需要从后往前遍历,逐步去掉所有结点的引用,否则无法遍历下取 while(head.next != null) { for (int i = size(); i > 1; i--) { temp = temp.next; } temp.next = null; } return true; } }
略……逻辑思路都差不多,只是将头节点换成一个头指针
不带头节点和带头结点的主要区别:带头结点遍历的时候、不能将头节点进行计数;而不带结点能够直接进行遍历
本质上两者并没有什么太大区别,带头节点的链表没有指针,头结点就相当于头指针,而不带头节点的链表是由头指针的
注意:这里所谓的指针和结点其实都是结点对象,只是指针初始值为null,结点要进行初始化
/** * 练习题1:获取链表倒数第k个结点 * 练习题2:将链表反转 * 练习题3:从尾到头打印链表的结点 * 练习题4:合并两个有序链表,合并后仍然有序 */ //测试类: public class SingleLinkedListDemo3{ public static void main(String[] args) { Node node1 = new Node(1); Node node2 = new Node(2); Node node3 = new Node(3); SingleLinkedList sll = new SingleLinkedList(); sll.add(node1); sll.add(node2); sll.add(node3); System.out.println("链表原始状态:"); sll.show(); System.out.println("------------------------"); //测试1:测试获取链表倒数第k个结点 Node t = sll.findLastIndexNode(sll, 1); System.out.println(sll.findLastIndexNode(sll,1)); System.out.println(sll.findLastIndexNode(sll,2)); System.out.println(sll.findLastIndexNode(sll,3)); System.out.println("-------------------------"); //测试2:测试将链表反转 sll.reverset(sll); System.out.println("反转后的链表:"); sll.show(); System.out.println("-------------------------"); //测试3:从头到位打印链表 System.out.println("反向打印链表:"); sll.reversetPrint(sll); System.out.println("-------------------------"); //测试4:将两个有序链表合并,合并后仍然有序 SingleLinkedList list1 = new SingleLinkedList(); SingleLinkedList list2 = new SingleLinkedList(); Node node4 = new Node(4); Node node5 = new Node(5); Node node6 = new Node(6); Node node7 = new Node(7); Node node8 = new Node(8); Node node9 = new Node(9); Node node10 = new Node(10); Node node11 = new Node(11); list1.add(node4); list1.add(node7); list1.add(node8); list1.add(node10); list1.add(node11); System.out.println("链表1:"); list1.show(); System.out.println("链表2:"); list2.add(node5); list2.add(node6); list2.add(node9); list2.show(); SingleLinkedList list = new SingleLinkedList(); list = list.combine(list1, list2); System.out.println("合并后的链表:"); list.show(); } }
package com.hhxy.linkedlist; import java.util.Stack; //结点类 class Node { int n; Node next; public Node(int n) { this.n = n; } @Override public String toString() { return "[n=" + n + "]"; } } //链表类: public class SingleLinkedList { //初始化头节点 public Node head = new Node(-99); /** * 添加结点 */ public void add(Node node) { Node current = head; while(current.next != null) { current = current.next; } current.next = node; } /** * 获取链表的长度 * @return */ public int size() { Node current = head.next; int count = 0; while(current != null) { count++; current = current.next; } return count; } /** * 展示 */ public void show() { Node current = head.next; while(current != null) { System.out.println(current); current = current.next; } } /*--------------------核心方法-------------------------*/ /** * 寻找链表倒数第k个结点 * @param index * @return */ public Node findLastIndexNode(SingleLinkedList sll,int index) { Node head = sll.head; if(index <0 || index>sll.size() || head.next == null) { return null; } Node current = head.next; //将指针从第二个结点开始往后移动index位 for (int i = 0; i < size()-index; i++) { current = current.next; } return current; } /** * 将链表反转 * @param sll 待反转的链表 */ public void reverset(SingleLinkedList sll) { Node head = sll.head; if(head.next == null || head.next.next == null) { //当前链表为空,或者只有一个结点,直接返回 return; } SingleLinkedList sllTemp = new SingleLinkedList();//创建一个新链表 Node headTemp = sllTemp.head; Node temp = null;//用来存旧链表的引用,方便遍历旧链表 Node current = head.next;//辅助遍历旧链表 while(current != null) { temp = current.next;//不保存,新链表就会断开,就无法进行遍历了 current.next = headTemp.next;//指向新创建的头结点的后面的结点 headTemp.next = current;//新创建的头结点,指向插入的结点 current = temp;//指针往后移 } head.next = headTemp.next; } /** * 反向打印链表 * @param sll */ public void reversetPrint(SingleLinkedList sll) { Node head = sll.head; if(head.next == null) { return; } /* //方式一:使用findLastIndexNode方法(要先实现findLastIndexNode方法,不值得推荐) for(int i=1;i<=sll.size();i++) { System.out.println(sll.findLastIndexNode(sll, i)); } //方式二:使用reverset方法(要先实现reverset方法,并且改变了链表的结构,不值得推荐) reverset(sll); sll.show(); */ //方式三:使用栈(强烈推荐) Stack<Node> stack = new Stack<>(); Node current = head.next; while(current != null) { stack.push(current); current = current.next; } while(stack.size()>0) { System.out.println(stack.pop()); } } /** * 合并两个有序链表,合并后仍然有序(这里我是默认按从小到大排序的) */ public SingleLinkedList combine(SingleLinkedList sll1,SingleLinkedList sll2) { Node head1 = sll1.head.next;//用于遍历sll1链表 Node head2 = sll2.head.next; if(head1 == null || head2 == null) { //只要有一个链表为空就直接返回 return head1 != null ? sll1 : sll2; } SingleLinkedList sll = new SingleLinkedList();//合并后的链表 Node temp=sll.head;//用来给sll链表添加结点的 while (head1 != null && head2 != null){ if (head1.n < head2.n){ //链表1的结点是当前最小结点 temp.next = head1;//新链表连接最小结点 temp = temp.next;//每新增一个结点,temp就往后移一位,保证他在尾结点方便连接新结点 head1 = head1.next;//链表1的指针也往后移一位 }else{ //链表2的结点是当前最小结点 temp.next = head2; temp = temp.next; head2 = head2.next; } } if (head1 != null && head2 == null){ //经过一一段时间的合并后,sll2的链表为空了,直接就将sll1链表后面的结点拼接上去 temp.next = head1; } if (head1 == null && head2 != null){ temp.next = head2; } return sll; } /*------------------------------------------------*/ }
双向链表相对单向链表就较为简单了,因为每个结点既能往后遍历,又能往前遍历 ,对于插入、删除、修改都无需像单链表一样依靠前一个结点。
与单链表的主要区别:
遍历不仅可以往前遍历,还可以往后遍历
插入、删除、修改不需要依赖前一个结点(在链尾插入需要依赖尾结点)
添加的时候,需要进行双向绑定!
双向链表插入示意图:
双向链表删除示意图:
和单向链表的测试方法相同
示意图:
其实只要理解了单向链表,再来看双向链表就会觉得so easy声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。