目录
1、基本概念
链表是一种链式存储的数据结构,用一组地址任意的存储单元存放线性表中的数据结构。链表分很多种,比如单链表、双链表等,单链表的设计思想是最基础的,相关算法的实现可能也比双链表等要复杂一点,所以本文以单链表为例讲解。单链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据,即单链表中每个节点都包含data域和next域,如下图所示。
单链表的逻辑结构示意图如下:
为了方便理解,小编对单链表总结出了以下几点:
①链表是以节点的方式来存储的;
②单链表每个节点包含data域(存放数据)和next域(指向下一个节点),双链表还包含pre域(指向上一个节点);
③各个节点不一定是顺序存储的,即下个节点的位置并不是一定在该节点的下一个地址,也就是说链表中数据元素的逻辑是有序的,而数据元素在物理存储单元上是无序的;
④链表分头节点是null和头节点不是null两种,根据代码需求选择
单链表的节点结构:
class Node<V>{
V data;
Node next;
}
双链表的节点结构中比单链表多了一个Node类型的pre变量:
class Node<V>{
V data;
Node next;
Node pre;
}
2、代码实现单链表(包括增删改查)
下面通过一个例子讲解怎么用代码自己写一个简单的单链表出来,并且实现增删改查功能。
//测试代码
public class SingleLinkedListDemo {
public static void main(String[] args) {
SingleLinkedList singleLinkedList = new SingleLinkedList();
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode newHero2 = new HeroNode(2, "卢哥", "火麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
//不考虑排序的加入方式
singleLinkedList.add(hero1);
singleLinkedList.add(hero2);
singleLinkedList.add(hero3);
singleLinkedList.add(hero4);
singleLinkedList.showSingleLinkedList();
System.out.println("==========================================");
singleLinkedList.reverseList(singleLinkedList.head);
singleLinkedList.showSingleLinkedList();
//考虑排序的加入方式
/*singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.showSingleLinkedList();*/
//修改单链表
/*System.out.println("==========================================");
System.out.println("修改后的单链表");
singleLinkedList.update(newHero2);
singleLinkedList.showSingleLinkedList();*/
//删除单链表
/*System.out.println("==========================================");
System.out.println("删除后的单链表");
singleLinkedList.del(2);
singleLinkedList.showSingleLinkedList();*/
}
}
//单链表
class SingleLinkedList {
public SingleLinkedList() {
}
HeroNode head = new HeroNode(0, "", "");
//无顺序添加
public void add(HeroNode heroNode) {//英雄节点直接添加在单链表的最后,不考虑排序问题
HeroNode temp = head;
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = heroNode;
}
//有顺序添加
public void addByOrder(HeroNode heroNode) {//考虑排序问题,讲添加的英雄节点加在指定位置
HeroNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {//表示temp已经在链表的最后位置,直接在该temp位置插入
break;
}
if (temp.next.no > heroNode.no) {//表示位置(中间位置)已找到,就在temp处插入
break;
}
if (temp.next.no == heroNode.no) {//表示添加的英雄节点的编号已经在单链表中出现
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("编号存在,添加失败");
} else {
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改节点信息,根据节点编号no修改
public void update(HeroNode newHeroNode) {
if (head.next == null) {
System.out.println("单链表为空");
return;//这条语句的作用是直接结束,不执行下面的程序
}
HeroNode temp = head.next;
boolean flag = false;
while (true) {
if (temp == null) {
break;
}
if (temp.no == newHeroNode.no) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
} else {
System.out.printf("编号为%d的节点没有找到,不能修改", newHeroNode.no);
}
}
//删除节点
public void del(int no) {
HeroNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no == no) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.next = temp.next.next;
} else {
System.out.printf("没有找到编号为%d的节点", no);
}
}
//显示单链表
public void showSingleLinkedList() {
if (head.next == null) {
System.out.println("单链表是空的");
return;
}
HeroNode temp = head.next;
while (true) {
if (temp == null) {
break;
}
System.out.println(temp);
temp = temp.next;
}
}
}
//创建一个类HeroNode,用来代表单链表中的各个节点
class HeroNode {
public int no;
public String name;
public String nickName;
public HeroNode next;
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
//为了显示英雄信息,可以不用再写一个show()方法,直接重写toString()方法即可
//重写之后,直接println一个HeroNode变量时,就会按照重写的格式输出,而不是按照默认格式
//不重写toString()方法那么系统有自己的默认输出,具体见课本359页
@Override
public String toString() {
return "HeroNode{ no=" + no + ", name='" + name + '\'' + ", " +
"nickName='" + nickName + '\'' + '}';
}
}
3、单链表中的经典题型
3.1 反转单链表
思路:
①先创建一个新的头节点对象newHead;
②定义两个辅助变量cur、temp,用来遍历单链表,cur用来指向当前节点,temp用来记录当前节点的下一个节点;
③遍历到当前节点就将该节点取出,并放在新头节点后面(cur.next=newHead.next;newHead.next=cur 即取出的节点前后都要连起来且必须先把该节点后面那根线连起来,再连前面那根线,顺序不能换)
④把原来链表的head.next和newHead.next相连即可
代码实现如下:
//将单链表反转
public void reverseList(HeroNode head) {
if (head.next == null || head.next.next == null) {
return;
}
HeroNode cur = head.next;
HeroNode temp = null;
HeroNode newHead = new HeroNode(0, "", "");
while (cur != null) {
temp = cur.next;
cur.next = newHead.next;
newHead.next = cur;
cur = temp;
}
head.next = newHead.next;
}
3.2 判断单链表是否是回文结构
该题型可以用三种方法实现:
方法1(适合笔试,笔试时间复杂度低就行,这个方法空间复杂度高,但是代码最容易):开辟一块栈空间(栈的特点:先进后出);
方法2:使用快慢指针(起点不同,快慢指针的策略需要自己思考)+栈;
方法3(适合面试,3种方法的时间复杂度都差不多,但方法3空间复杂度最低o(1),只有有限几个变量):只使用快慢指针,不借用其他数据结构。
import java.util.Stack;
public class Question1 {
//这里的head都是value有值的节点
//方法1(适合笔试,笔试时间复杂度低就行,这个方法空间复杂度高,但是代码最容易):开辟一块栈空间(栈的特点:先进后出)
public static boolean isPalindrome1(Node head) {
Stack<Node> stack = new Stack<>();
Node cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
while (head != null) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
//方法2:使用快慢指针(起点不同,快慢指针的策略需要自己思考)+栈
public static boolean isPalindrome2(Node head) {
if (head == null || head.next == null) {
return true;
}
Node right = head.next;
Node cur = head;
while (cur.next != null && cur.next.next != null) {
right = right.next;
cur = cur.next.next;
}
Stack<Node> stack = new Stack<>();
while (right != null) {
stack.push(right);
right = right.next;
}
while (!stack.isEmpty()) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
//方法3(适合面试,3种方法的时间复杂度都差不多,但方法3空间复杂度最低o(1),只有有限几个变量):只使用快慢指针,不借用其他数据结构
public static boolean isPalindrome3(Node head) {
if (head == null || head.next == null) {
return true;
}
//n1慢指针,一次一步,n2快指针,一次两步,等走完之后,n1指的位置正好是链表中点位置
Node n1 = head;
Node n2 = head;
while (n2.next != null && n2.next.next != null) {
n1 = n1.next;
n2 = n2.next.next;
}
//将n1之后的节点逆序,n1.next指向null
n2 = n1.next;
n1.next = null;
Node n3 = null;
while (n2 != null) {
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
//进行比对
n3 = n1;
n2 = head;
boolean res = true;
while (n1 != null && n2 != null) {
if (n1.value != n2.value) {
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
//将后半段链表逆序回来,恢复原样
n1 = n3.next;
n3.next = null;
while (n1 != null) {
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
//测试代码
public static void main(String[] args) {
Node data1 = new Node(6);
Node data2 = new Node(8);
Node data3 = new Node(9);
Node data4 = new Node(28);
Node data5 = new Node(9);
Node data6 = new Node(8);
Node data7 = new Node(6);
SingleLinkedList singleLinkedList = new SingleLinkedList();
singleLinkedList.add(data1);
singleLinkedList.add(data2);
singleLinkedList.add(data3);
singleLinkedList.add(data4);
singleLinkedList.add(data5);
singleLinkedList.add(data6);
singleLinkedList.add(data7);
singleLinkedList.showSingleLinkedList();
System.out.println(isPalindrome3(data1));
}
}
class Node {
int value;
Node next;
public Node(int value) {
this.value = value;
}
public Node() {
}
//为了显示英雄信息,可以不用再写一个show()方法,直接重写toString()方法即可
//重写之后,直接println一个HeroNode变量时,就会按照重写的格式输出,而不是按照默认格式
//不重写toString()方法那么系统有自己的默认输出,具体见课本359页
@Override
public String toString() {
return "Node{ value=" + value + '}';
}
}
//作用:创建单链表(多个节点连接起来)
class SingleLinkedList {
public SingleLinkedList() {
}
//这里的head是空节点
Node head = new Node();
//无顺序添加
public void add(Node node) {//英雄节点直接添加在单链表的最后,不考虑排序问题
Node temp = head;
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = node;
}
//显示单链表
public void showSingleLinkedList() {
if (head.next == null) {
System.out.println("单链表是空的");
return;
}
Node temp = head.next;
while (true) {
if (temp == null) {
break;
}
System.out.println(temp);
temp = temp.next;
}
}
}
3.3 复制含有随机指针节点的链表
特殊节点定义
class Node{
int value;
Node next;
Node rand;//rand指针可能指向链表中任意一个节点,也可能指向null
}
方法①(适合笔试):创建哈希表,因为哈希表有键值对结构,原链表当作key,复制的链表放在value位置;
方法②(适合面试,不借用其他数据结构,但代码复杂):复制第一个节点直接放在第一个节点后面,第二个节点前面,依次下去,单链表变成了2n个节点,再分别确定next和rand,最后分离原链表和复制的链表。
方法①的代码实现如下:
import java.util.HashMap;
public class Question3 {
public static void main(String[] args) {
//测试代码自己写
//先创建一个由随机节点组成的单链表,往单链表中加节点
//然后在调用copyLinkedList1函数
}
//使用哈希表复制单链表,哈希表中存放键值对(java中叫Entry)
public static RandomNode copyLinkedList1(RandomNode head){
HashMap<RandomNode,RandomNode> map=new HashMap<>();
RandomNode cur=head;
while (cur!=null){
map.put(cur,new RandomNode(cur.value));
cur=cur.next;
}
cur=head;
while (cur!=null){
//cur 老节点
//map.get(cur) 新节点,也就是复制的节点
map.get(cur).next=map.get(cur.next);
map.get(cur).rand=map.get(cur.rand);
cur=cur.next;
}
return map.get(head);
}
}
class RandomNode{
int value;
RandomNode next;
RandomNode rand;
public RandomNode() {
}
public RandomNode(int value) {
this.value = value;
}
}