一、链表
1.1 链表的概念
链表(Linked List)是一种常见的数据结构,由一系列的节点(Node)组成,每个节点包含两个部分:数据和指针。数据部分存储着具体的数据,指针部分存储着下一个节点的地址(即指向下一个节点的指针)。通过这种方式,多个节点可以组合成一个链式结构。
链表可以分为单向链表、双向链表、循环链表等几种类型。单向链表的每个节点只有一个指向下一个节点的指针;双向链表的每个节点有两个指针,一个指向前一个节点,一个指向后一个节点;循环链表则是在单向或双向链表的基础上,将最后一个节点的指针指向第一个节点或者将第一个节点的前驱指针指向最后一个节点,从而形成一个环。
1.2 链表的优缺点
**链表的主要优点是插入和删除操作比较容易,因为只需要调整相应节点的指针即可,**而不需要像数组一样移动大量数据。
**链表的缺点是访问节点时需要从头开始遍历,因此访问时间比较慢。**此外,链表需要额外的指针来存储节点的地址信息,因此会占用更多的内存空间。
二、单向链表
2.1 单向链表介绍
单向链表是一种常见的数据结构,由节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。
在单向链表中,每个节点只能访问它后面的节点,而不能访问它前面的节点。链表的第一个节点称为头节点,最后一个节点称为尾节点,尾节点的指针指向空。
单向链表的操作包括插入、删除和遍历等。
插入操作可以在链表的任意位置插入一个新节点,只需要将新节点的指针指向原来节点的后继节点,然后将原来节点的指针指向新节点。
删除操作可以删除链表中的任意节点,只需要将被删除节点的前驱节点的指针指向被删除节点的后继节点,然后释放被删除节点的内存空间。
遍历操作可以依次访问链表中的每个节点,可以用循环或递归的方式实现。
单向链表的优点是插入和删除操作的时间复杂度为 O(1),缺点是访问任意节点的时间复杂度为 O(n)。
2.2 单向链表应用
单向链表在计算机科学中有很多应用,以下列举几个常见的应用:
- **实现栈和队列:单向链表可以用来实现栈和队列,**因为它支持常数时间的元素插入和删除操作。
- **内存管理:**许多编程语言和操作系统都使用单向链表来管理内存。每个链表节点表示一个空闲内存块,可以快速插入和删除节点,以及寻找满足内存需求的空闲块。
- **浏览器历史记录:**浏览器历史记录可以用单向链表来实现,**每个节点表示一个访问过的页面,链表头表示最近访问的页面,链表尾表示最早访问的页面。 **
- **图形处理:**在计算机图形学中,**单向链表可以用来表示多边形的顶点。每个节点包含顶点的坐标和指向下一个顶点的指针,可以快速遍历所有顶点。 **
- **文件系统:**文件系统中的目录和文件也可以用单向链表来表示。**每个节点表示一个目录或文件,指针指向下一个节点或子节点。 **
单向链表是一种非常基础和灵活的数据结构,可以用于许多不同的应用场景。
三、实例挑战
给出两个递增的单向链表,请将其合并成新的递增链表
3.1 思路方法一:使用双指针迭代
两个链表都是递增的,使用两个指针分别指向两个链表的头部,然后进行比较,取出最小的元素 ,放入新的链表中,指针后移,两个链表依次比较。(两个指针同向访问)
Step:
1、判断链表是否为空
2、新建新链表节点,在其后插入值
3、遍历链表,取较小值添加在新链表节点后面
4、遍历之后还会有一个链表 有剩余的节点,直接将其拼接到新链表后面
时间复杂度O(n);
空间复杂度O(1);
3.1.1 定义单向链表节点类
//单向链表中每个节点 都是由一个值和一个指针组成,指针指向下一个节点。
//将多个节点连接起来就是一个单向链表
public class ListNode {
int val; //该节点存储的值
ListNode next = null; //该节点指向下个节点的指针。如果next指针为空,说明它是尾节点
//构造参数,接收一个值val,将它赋值给该节点的val
ListNode(int val) {
//this指的是当前对象,当前的对象是ListNode
this.val = val;
}
}
3.1.2 合并链表
public static void main(String[] args) {
//新建list1 、list2
ListNode list1 = new ListNode(1);
list1.next = new ListNode(3);
list1.next.next = new ListNode(5);
ListNode list2 =new ListNode(2);
list2.next =new ListNode(4);
list2.next.next = new ListNode(6);
//调用函数拼接list1、list2
ListNode resultList = Merge(list1,list2);
//遍历新链表
ListNode cur = resultList;
System.out.println("方法一:双指针迭代");
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
}
public static ListNode Merge(ListNode list1, ListNode list2){
//首先判断两个链表是不是为空,如果一个表为空,那最后的结果就是另一个表
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
//新建一个节点,让它作为新链表的表头,给它赋值为0
ListNode head = new ListNode(0);
// 新建一个遍历链表的指针,初始时让该指针指向链表的头部
// 因此,在创建指针变量时,我们需要为它指定一个类型,以便让它能够指向该类型的对象。
// 由于链表中的每个节点都是一个ListNode对象,因此指针变量的类型也应该是ListNode。
// 这样,我们就可以使用cur指针来指向链表中的每一个节点,访问它们的值和指针。
ListNode cur = head;
//当两个链表都不是空的时候
while(list1 != null && list2 != null){
//取较小的节点值
if(list1.val <= list2.val){
//如果lis1的值 《 list2的值,将cur指向list1,
cur.next = list1;
//然后将list1的指针往后移一位
list1 = list1.next;
}else {
//如果lis2的值 《 list1的值,将cur指向list2,然后将list2的指针后移一位
cur.next = list2;
list2 = list2.next;
}
//将cur指向新链表的下个节点,并将指针移动到下个节点
cur = cur.next;
}
/*
当链表被遍历到最后一个节点时,如果该节点的next指针指向的是null,表示这是链表的末尾。
所以如果list1 或者 list2 被遍历完后,next指向的是null,说明该链表已经遍历到末尾,没有节点可以继续遍历了。
因此,list1遍历完后 就会变成 null,表示它已经被遍历完了。
* **/
//循环完成,看哪个链表还有剩余的部分,因为两个链表都是有序的,如果有剩余的部分,就直接拼接到新链表的后面
if(list1 != null){
cur.next = list1;
}else {
cur.next =list2;
}
//返回新链表的头节点
return head.next;
}
输出结果
方法一:双指针迭代
1 2 3 4 5 6
3.2 思路方法二:使用双指针递归
添加完节点后,该节点的指针后移,相当于这个单链表剩余的部分与另一个链表剩余的部分继续合并,两个链表剩余的部分合并,可以使用递归。
Step:
1、判断链表是否为空
2、新建链表节点
3、每次比较两个链表当前节点值大小,取较小的插入新链表,将较小值的链表指针后移,另一个不变,两个子链表作为新的链表送入递归中。
4、递归的结果插入新链表
5、递归之后还会有一个链表 有剩余的节点,直接将其拼接到新链表后面
时间复杂度O(n);
空间复杂度O(1);
3.2.1 定义单向链表节点类
同3.1.1
3.2.2 递归合并链表
public static void main(String[] args) {
ListNode list1 = new ListNode(1);
list1.next = new ListNode(3);
list1.next.next = new ListNode(5);
ListNode list2 =new ListNode(2);
list2.next =new ListNode(4);
list2.next.next = new ListNode(6);
//递归合并
ListNode recursionList = MergeRecursion(list3,list4);
ListNode cur = recursionList;
System.out.println("方法二:双指针递归");
while(cur != null){
System.out.print(cur2.val + " ");
cur = cur.next;
}
}
public static ListNode MergeRecursion(ListNode list1, ListNode list2){
if(list1 == null){
return list2;
}
if(list2 == null){
return list1;
}
if(list1.val <= list2.val){
//进行递归
list1.next = MergeRecursion(list1.next,list2);
return list1;
}else {
list2.next = MergeRecursion(list1,list2.next);
return list2;
}
}
输出结果
方法二:双指针递归
1 2 3 4 5 6
四、实例挑战-判断链表中是否有环
判断链表中是否有环
4.1 思路:快慢指针
使用一个快指针每次走两步,使用慢指针每次走一步,如果链表中存在环,遍历的时候快指针和慢指针会在这个环中遍历,快指针比慢指针走的快,所以快指针会追上慢指针,即快慢指针相遇。如果没有环的话,一个快,一个慢,就相遇不了。
如图:
4.1.1 定义单向链表节点类
同 3.1.1
4.1.2 判断是否有环
主方法
public static void main(String[] args) {
// 构造链表
ListNode head = new ListNode(1);
ListNode node1 = new ListNode(2);
ListNode node2 = new ListNode(3);
ListNode node3 = new ListNode(4);
ListNode node4 = new ListNode(5);
head.next = node1;
node1.next = node2;
node2.next = node3;
node3.next = node4;
// 添加环
node4.next = node2;
// 判断是否存在环
boolean hasCycle = hasCycle(head);
System.out.println(hasCycle);
}
判断是否有环,若有环返回ture,否则返回false
public boolean hasCycle(ListNode head) {
//先判断链表是否为空
if(head == null){
return false;
}
//设置快慢双指针,一个一次移动2步,一个一次移动一步,如果有环的话,快慢指针会在环内相遇,如果没有环,快指针走到链表末尾未相遇。
//指针类型都是 ListNode类
ListNode fast = head;
ListNode slow = head;
//快慢指针进行遍历
/**
因为单向链表尾节点指针指向null
所以可以根据是否到尾节点 进行循环fast != null && fast.next != null
如果是单向链表 如果是奇数那fast.next指向的是空,链表元素为偶数则fast.next为空
*/
while(fast != null && fast.next != null){
//快指针 移动两步
fast = fast.next.next;
//慢指针移动一步
slow = slow.next;
//如果有环就会相遇,即 fast = slow
if(fast == slow){
return true;
}
}
return false;
}