文章目录
- 1、单链表定义
- 2、双链表定义
- 3、环形链表定义
- 3.1、环形链表代码实现
- 4、经典面试题
- 4.1、如何设计一个LRU缓存淘汰算法?
- 4.2、约瑟夫问题
- 4.3、单链表的反转(反转链表)(https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof)
- 4.4、查找链表的中间节点(https://leetcode-cn.com/problems/middle-of-the-linked-list/)
- 4.5、在O(1)时间删除单链表节点(https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof)
- 4.6、查找单链表倒数第k个节点(https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof)
- 4.7、合并两个有序的链表(https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof)
- 4.8、从尾到头打印单链表(https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof)
- 4.9、判断单链表是否有环(https://leetcode-cn.com/problems/linked-list-cycle/)
- 4.10、从有环链表中,获得环的长度
- 4.11、单链表中,取出环的起始点(https://leetcode-cn.com/problems/linked-list-cycle-ii/)
- 4.12、判断两个单链表相交的第一个交点(https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/)
- 4.13、复杂链表的复制(https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/)
1、单链表定义
采用的是链式存储结构,使用一组地址任意的存储单元来存放数据元素。在单链表中,存储的每一条数据都是以节点来表示的,每个节点的构成都是:元素(存储数据的存储单元)+指针(存储下一个节点的地址值),如下图所示:
首节点:单链表的开始节点
尾节点:单链表的终端节点
1.1、根据序号获取节点的操作(时间复杂度O(n))
在线性表中,每个节点都有一个唯一的序号,该序号是从0开始递增。通过序号获取单链表的节点时,我们需要从链表的首节点开始,从前往后开始循环遍历,直到遇到查询序号所对应的节点时为止。
以下图为例,我们需要获得序号为 2 的节点,那么就需要依次遍历获得“节点 1”和“节点 2”,然后才能获得序号为 2 的节点,也就是“节点 3”。
1.2、根据序号删除节点的操作(时间复杂度O(1))
首先根据序号获得需要删除的节点,然后让"删除的节点的前一个节点"指向"删除节点的后一个节点",这就实现了节点的删除操作。
以下图为例,我们需要删除序号为 2 的节点,那么就让“节点 2”指向“节点 4”即可,这样就删除了序号为 2 的节点,也就是删除了“节点 3”。
1.3、根据序号插入节点的操作(时间复杂度O(1))
根据序号插入节点的操作,我们首先应该根据序号找到插入的节点位置,然后让“插入位置的
上一个节点”指向“新插入的节点”,然后再让“新插入的节点”指向“插入位置的节点”,这样就实现了节点的插入操作。
以下图为例,我们需要在序号为 2 的位置插入元素值“0”,首先先把字符串“0”封装为一
个节点对象,然后就让“节点 2”指向“新节点 0”,最后再让“节点 0”指向“节点 3”,这样就插入了一个新节点。
1.4、顺序表(数组)和单链表(链表)的比较
(1) 存储方式比较
顺序表采用一组地址连续的存储单元依次存放数据元素,通过元素之间的先后顺序来确定元素
之间的位置,可以借助CPU的缓存机制,预读数组中的数据,因此访问效率更高,存储空间的利用率较高。
单链表采用一组地址任意的存储单元来存放数据元素,通过存储下一个节点的地址值来确定节
点之间的位置,对CPU缓存不友好,没办法有效预读,因此访问效率低,存储空间的利用率较低。
(2) 时间性能比较
顺序表查找的时间复杂度为 O(1),插入和删除需要移动元素,因此时间复杂度为 O(n)。若是需要频繁的执行查找操作,但是很少进行插入和删除操作,那么建议使用顺序表。
单链表查找的时间复杂度为 O(n),插入和删除无需移动元素,因此时间复杂度为 O(1)。若是需要频繁的执行插入和删除操作,但是很少进行查找操作,那么建议使用链表。
补充:根据序号来插入和删除节点,需要通过序号来找到插入和删除节点的位置,那么整体的
时间复杂度为 O(n)。因此,单链表适合数据量较小时的插入和删除操作,如果存储的数据量较大,那么就建议使用别的数据结构,例如使用二叉树来实现。
(3) 空间性能比较
顺序表需要预先分配一定长度的存储空间,如果事先不知道需要存储元素的个数,分配空间过
大就会造成存储空间的浪费,分配空间过小则需要执行耗时的扩容操作。
单链表不需要固定长度的存储空间,可根据需求来进行临时分配,只要有内存足够就可以分配,在链表中存储元素的个数是没有限制的,无需考虑扩容操作。
1.5单链表代码实现
/**
* @author :zhz
* @date :Created in 2020/12/22
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: node节点类
**/
public class ListNode {
int value;//值
ListNode next;//下一个指针
public ListNode(int value) {
this.value = value;
this.next=null;
}
}
/**
* @author :zhz
* @date :Created in 2020/12/22
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: 单链表实现
**/
public class MyLinkedList {
private ListNode head;
//插入链表的头部 data就是插入的数据
public void insertHead(int data){
//不管当前是否有数据,只需要把新节点的指针,指向已有的head,再把head指向新节点(栈内存的引用)
ListNode newNode=new ListNode(data);
newNode.next=head;
head=newNode;
}
//插入链表的中间 假设定义在第N个插入 O(n)
public void insertNth(int data,int position){
if (position==0){
insertHead(data);//头插入
}else {
ListNode cur=head;
for (int i = 0; i < position; i++) {
cur=cur.next;//向后遍历到插入元素的前一个节点
}
ListNode newNode=new ListNode(data);//创建新节点
//插入操作
newNode.next=cur.next;
cur.next=newNode;
}
}
//删除头结点
public void deleteHead(){//O(1)
head=head.next;
}
//删除链表的中间
public void deleteNth(int position) {//O(n)
if (position==0){
deleteHead();
}else{
ListNode cur=head;
for (int i = 0; i < position; i++) {
cur=cur.next;//向后遍历到插入元素的前一个节点
}
//删除操作
cur.next=cur.next.next;//cur.next 表示的是删除的点,后一个next就是我们要指向的
}
}
//找节点
public void find(int data) {//O(n)
ListNode cur=head;
while (cur!=null){
if(cur.value==data){
break;
}
cur=cur.next;
}
}
//打印节点
public void print(){
ListNode cur=head;
while (cur!=null){
System.out.print(cur.value+" ");
cur=cur.next;
}
System.out.println();
}
public static void main(String[] args) {
MyLinkedList myList = new MyLinkedList();
myList.insertHead(5);
myList.insertHead(7);
myList.insertHead(10);
myList.print(); // 10 -> 7 -> 5
// myList.deleteNth(0);
// myList.print(); // 7 -> 5
// myList.deleteHead();
// myList.print(); // 5
// myList.insertNth(11, 1);
// myList.print(); // 5 -> 11
// myList.deleteNth(1);
// myList.print(); // 5
}
}
2、双链表定义
也叫双向链表,采用的是链式储存处结构。在双链表中,每个节点中都有两个指针,分别指向直接前驱节点(保存前一个节点的地址值)和直接后继节点(保存后一个节点的地址值),如下图所示
2.1、特点
双链表的任意一个节点,都可以很方便的访问它的直接前驱节点和直接后驱节点,如下图所示
2.2、单链表和双链表的区别
逻辑上无区别,他们均是完成线性表的内容,主要的区别是结构上的构造有所区别
1、单链表
对于一个节点,有储存数据的 data 和指向下一个节点的 next。也就是说,单链表的遍历操作都得通过前节点—>后节点。
2、 双链表
对于一个节点,有储存数据的 data 和指向下一个节点的 next,还有一个指向前一个节点的 pre。也就是说,双链表不但可以通过前节点—>后节点,还可以通过后节点—>前节点。
2.3、双链表代码实现
/**
* @author :zhz
* @date :Created in 2020/12/22
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: 双链表节点类
**/
public class DNode {
int value; //值
DNode next; //下一个的指针
DNode pre; //指向的是前一个指针
DNode(int value){
this.value = value;
this.next = null;
this.pre = null;
}
}
/**
* @author :zhz
* @date :Created in 2020/12/22
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: 双链表实现
**/
public class DoubleLinkList { // 双向链表
private DNode head; //头
private DNode tail; //尾
public DoubleLinkList() {
head=null;
tail=null;
}
public void insertHead(int data){//插入头部
DNode newNode=new DNode(data);
if (head==null){
tail=newNode;
}else{
head.pre=newNode;
newNode.next=head;
}
head=newNode;
}
public void deleteHead(){//删除头部
if (head==null){//没有数据
return;
}
if(head.next==null){//表示只有头节点
tail=null;
}else{
head.next.pre=null;
}
head=head.next;//把第二个节点置为当前头节点
}
public void deleteKey(int data){//删除指定的数据
DNode current=head;
while (current.value!=data){
if (current.next==null){
System.out.println("无节点");
return;
}
current=current.next;//向后移一位
}
if (current==head){//表示删除头节点
deleteHead();
}else{
current.pre.next=current.next;
if (current==tail){//删除尾部
tail=current.pre;
current.pre=null;
}else{
current.next.pre=current.pre;
}
}
}
}
3、环形链表定义
环形链表依旧采用的是链式存储结构,它的特点就是设置首节点和尾节点相互指向,从而实现
让整个链表形成一个环。常见的环形链表有:
(1) 环形单链表
在单链表中,尾节点的指针指向了首节点,从而整个链表形成一个环,如下图所示:
(2) 环形双链表
在双链表中,尾节点的指针指向了首节点,首节点的指针指向了尾节点,从而整个链表形成一
个环,如下图所示:
3.1、环形链表代码实现
/**
* @author :zhz
* @date :Created in 2020/12/22
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: 环形单链表的实现
**/
public class CycleSingleLinkedList {
/**
* 用于保存单链表中的首节点
*/
private CycleNode headNode;
/**
* 用于保存单链表中的尾节点
*/
private CycleNode lastNode;
/**
* 用于保存单链表中节点的个数
*/
private int size;
/**
* 添加元素
*
* @param element 需要添加的数据
*/
public void add(Object element) {
// 1.把需要添加的数据封装成节点对象
CycleNode node = new CycleNode(element);
// 2.处理单链表为空表的情况
if (headNode == null) {
// 2.1 把 node 节点设置为单链表的首节点
headNode = node;
// 2.2 把 node 节点设置为单链表的尾节点
lastNode = node;
} else {// 3.处理单链表不是空表的情况
// 3.1 让 lastNode 指向 node 节点
lastNode.next = node;
// 3.2 更新 lastNode 的值
lastNode = node;
}
// 4.设置 lastNode 的 next 值为 headNode
lastNode.next = headNode;
// 5.更新 size 的值
size++;
}
/**
* 根据序号获取元素
*
* @param index 序号
* @return 序号所对应节点的数据值
*/
public Object get(int index) {
// 1.如果序号的取值小于 0,则证明是不合法的情况
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("序号不合法,index:" + index);
}
// 2.根据序号获得对应的节点对象
CycleNode node = node(index);
// 3.获取并返回 node 节点的数据值
return node.data;
}
/**
* 根据序号删除元素
*
* @param index 序号
*/
public void remove(int index) {
// 1.判断序号是否合法,合法取值范围:[0, size - 1]
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("序号不合法,index:" + index);
}
// 2.处理删除节点在开头的情况
if (index == 0) {
// 2.1 获得删除节点的后一个节点
CycleNode nodeNext = headNode.next;
// 2.2 设置 headNode 的 next 值为 null
headNode.next = null;
// 2.3 设置 nextNode 为单链表的首节点
headNode = nodeNext;
// 2.4 设置 lastNode 的 next 值为 headNode
lastNode.next = headNode;
} else if (index == size - 1) {// 3.处理删除节点在末尾的情况
// 3.1 获得删除节点的前一个节点
CycleNode preNode = node(index - 1);
// 3.2 设置 preNode 的 next 值为 null
preNode.next = null;
// 3.3 设置 preNode 为单链表的尾节点
lastNode = preNode;
// 3.4 设置 lastNode 的 next 值为 headNode
lastNode.next = headNode;
} else { // 4.处理删除节点在中间的情况
// 4.1 获得 index-1 所对应的节点对象
CycleNode preNode = node(index - 1);
// 4.2 获得 index+1 所对应的节点对象
CycleNode nodeNext = preNode.next.next;//或者 CycleNode nodeNext= node(index+1);
// 4.3 获得删除节点并设置 next 值为 null
preNode.next.next = null;//促进gc
// 4.4 设置 preNode 的 next 值为 nextNode
preNode.next = nodeNext;
}
// 5.更新 size 的值
size--;
// 6.判断 size 的值是否为 0,如果 size 的值为 0,则设置 headNode 和 lastNode 为null
if (size == 0) {
headNode = null;
lastNode = null;
}
}
/**
* 根据序号插入元素
*
* @param index 序号
* @param element 需要插入的数据
*/
public void add(int index, Object element) {
// 1.判断序号是否合法,合法取值范围:[0, size]
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("序号不合法,index:" + index);
}
// 2.把需要添加的数据封装成节点对象
CycleNode curNode = new CycleNode(element);
if (index==0){// 3.处理插入节点在开头位置的情况
// 3.1 设置 node 的 next 值为 headNode
curNode.next=headNode;
// 3.2 设置 node 节点为单链表的首节点
headNode= curNode;
// 3.3 设置 lastNode 的 next 值为 headNode
lastNode.next=headNode;
}else if (index==size-1){// 4.处理插入节点在末尾位置的情况
// 4.1 设置 lastNode 的 next 值为 node
lastNode.next=curNode;
// 4.2 设置 node 节点为单链表的尾节点
lastNode=curNode;
// 4.3 设置 lastNode 的 next 值为 headNode
lastNode.next=headNode;
}else{// 5.处理插入节点在中间位置的情况
// 5.1 获得 index-1 所对应的节点对象
CycleNode preNode = node(index - 1);
// 5.2 获得 index 所对应的节点对象
CycleNode cNode = preNode.next;
// 5.3 设置 preNode 的 next 为 node
preNode.next=curNode;
// 5.4 设置 node 的 next 为 curNode
curNode.next=cNode;
}
// 6.更新 size 的值
size++;
}
/**
* 根据序号获得对应的节点对象
*
* @param index 序号
* @return 序号对应的节点对象
*/
private CycleNode node(int index) {
// 1.判断环形单链表是否为空表
if (headNode == null) {
throw new NullPointerException("环形单链表为空表");
}
// 2.定义一个零时节点,用于辅助单链表的遍历操作
CycleNode tempNode = headNode;
// 3.定义一个循环,用于获取 index 对应的节点对象
for (int i = 0; i < index % size; i++) {
// 4.更新 tempNode 的值
tempNode = tempNode.next;
}
// 5.返回 index 对应的节点对象
return tempNode;
}
}
4、经典面试题
4.1、如何设计一个LRU缓存淘汰算法?
//实现一:利用LinkedHashMap
import sun.misc.LRUCache;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
/**
* @author zhz
* @date 2020/06/03
* 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
*
* 获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
* 写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。
* 当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
**/
public class _146_LRU缓存机制 extends LinkedHashMap<Integer, Integer> {
private int capacity;
/**
* 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
* LinkedHashMap是默认返回false的,我们可以继承LinkedHashMap然后复写该方法即可
* 例如 LeetCode 第 146 题就是采用该种方法,直接 return size() > capacity;
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size()>capacity;
}
public _146_LRU缓存机制(int capacity) {
super(capacity,0.75F,true);
this.capacity=capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key,value);
}
public static void main(String[] args) {
_146_LRU缓存机制 cache = new _146_LRU缓存机制( 2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1));
cache.put(3, 3); // 该操作会使得关键字 2 作废
System.out.println(cache.get(2));
cache.put(4, 4); // 该操作会使得关键字 1 作废
System.out.println(cache.get(1));
System.out.println(cache.get(3));
System.out.println(cache.get(4));
}
}
//实现方式二,维护一个有序的单链表,而有序就是加入的时间排序
4.2、约瑟夫问题
描述:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
现在问你最后留下的人是谁?
比如N=6,M=5
留下的就是1
1 2 3 4 5 6 => 6 1 2 3 4 => 6 1 2 3 =>1 2 3 => 1 3 => 1
/**
* @author :zhz
* @date :Created in 2020/12/23
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: 环形单链表的约瑟夫问题
**/
public class Josephus {
public static void main(String[] args) {
// 创建一个环形单链表
Node lastNode = new Node(10);
Node node9 = new Node(9, lastNode);
Node node8 = new Node(8, node9);
Node node7 = new Node(7, node8);
Node node6 = new Node(6, node7);
Node node5 = new Node(5, node6);
Node node4 = new Node(4, node5);
Node node3 = new Node(3, node4);
Node node2 = new Node(2, node3);
Node headNode = new Node(1, node2);
lastNode.next = headNode;
// 执行约瑟夫方法
josephus(headNode, lastNode, 10, 2, 3);
}
/**
* 约瑟夫问题
*
* @param headNode 环形单链表的首节点
* @param lastNode 环形单链表的尾节点
* @param size 环形单链表中节点的个数
* @param start 从编号为 start 的小孩开始报数
* @param count 每次数几下
*/
private static void josephus(Node headNode, Node lastNode, int size, int start, int count) {
// 1.处理不合法的情况
// 1.1 处理 headNode 为 null 的情况
if (headNode == null) {
throw new NullPointerException("headNode 为 null");
}
// 1.2 处理 start 和 count 不合法的情况
if (start > size || count > size) {
throw new IllegalArgumentException("参数不合法");
}
// 2.设置编号为 start 的小孩开始报数,并且使用 headNode 指向该节点
for (int i = 0; i < start - 1; i++) {// 把 headNode 和 lastNode 往后移动 start-1 次
headNode = headNode.next;
lastNode = lastNode.next;
}
// 3.定义一个循环,用于循环的执行报数操作
while (size != 0) { // 如果 size 等于 0,则停止报数
// 4.执行报数操作,也就是找到需要出圈的小孩,我们使用 headNode 指向需要出圈的节点
// 把 headNode 和 lastNode 往后移动 count-1 次
for (int i = 0; i < count - 1; i++) {
headNode = headNode.next;
lastNode = lastNode.next;
}
// 5.输出需要出圈小孩的编号,也就是输出 headNode 保存的数据值
System.out.println(headNode.data);
// 6.实现小孩的出圈操作,也就是把 headNode 从环形单链表中删除
// 6.1 获得删除节点的后一个节点
Node nextNode = headNode.next;
// 6.2 设置 lastNode 的 next 值为 nextNode
lastNode.next = nextNode;
// 6.3 设置 headNode 的 next 值为 null
headNode.next = null;//促进gc
// 7.更新 size 的值
size--;
// 8.设置 headNode 指向 nextNode
headNode = nextNode;
}
}
/**
* 节点类
*/
private static class Node {
/**
* 用于保存节点中的数据值
*/
private Object data;
/**
* 用于保存下一个节点的地址值
*/
private Node next;
/**
* 专门为 data 做初始化的工作
*
* @param data
*/
public Node(Object data) {
this.data = data;
}
/**
* 专门为 data 和 next 做初始化的工作
*
* @param data
* @param next
*/
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
}
}
4.3、单链表的反转(反转链表)(https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof)
/**
* @author zhz
* @date 2020/04/29
* 反转链表
**/
public class ReverseList {
/**
*解题思路:递归(从下往上看),假设我们 输入: 1->2->3->4->5->NULL,然后我们从5开始看,
*我们创建一个新的临时链表来存储返回的数据new_list,当new_list=5,输出new_list=5,因为head的下
*一个的为null了,然后我们继续递归new_list=4,
*
*
*/
public ListNode reverseList(ListNode head) {
//递归
if(head==null || head.next==null){
return head;
}
ListNode new_list=reverseList(head.next);//除了头结点外的其他都反转1->2<-3<-4<-5
head.next.next=head;
head.next=null;
return new_list;
}
//三指针迭代遍历
public ListNode reverseList1(ListNode head) {
if(head==null){
return head;
}
ListNode tail=null;
ListNode first=head;
ListNode second=head.next;
while(first!=null){
first.next=tail;//头节点的下一个节点是尾节点
tail=first; //尾节点变为头节点
first=second;//让原先的第二个节点赋值给头节点
if(second!=null){
second=second.next;
}
}
return tail;
}
/**
* 迭代
* @param head
* @return
*/
public ListNode reverseLinkedList(ListNode head){
ListNode cur = head, pre = null;
while(cur != null) {
ListNode tmp = cur.next; // 暂存后继节点 cur.next
cur.next = pre; // 修改 next 引用指向
pre = cur; // pre 暂存 cur
cur = tmp; // cur 访问下一节点
}
return pre;
}
}
4.4、查找链表的中间节点(https://leetcode-cn.com/problems/middle-of-the-linked-list/)
/**
* @author :zhz
* @date :Created in 2020/12/23
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: 链表的中间节点
**/
public class MiddleOfTheLinkedList {
//第一种:数组法(时间复杂度O(n),空间复杂度O(n))
public ListNode middleNode(ListNode head) {
ListNode[] nodes = new ListNode[100];
int index = 0;
while (head != null) {
nodes[index++] = head;
head = head.next;
}
return nodes[index / 2];
}
//快慢指针
public ListNode middleNode1(ListNode head) {
ListNode first = head;
ListNode last = head;
while (last != null && last.next != null) {
first = first.next;
last = last.next.next;
}
return first;
}
}
4.5、在O(1)时间删除单链表节点(https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof)
/**
* @author :zhz
* @date :Created in 2020/12/24
* @version: V1.0
* @slogan: 天下风云出我辈,一入代码岁月催
* @description: 在O(1)时间删除单链表节点
**/
public class DeleteNode {
public ListNode deleteNode(ListNode head, int val) {
if(head.value==val){
return head.next;
}
ListNode pre=head;
ListNode cur=head.next;
while (cur!=null&&cur.value!=val){
pre=cur;
cur=cur.next;
}
if (cur!=null){
pre.next=cur.next;
}
return head;
}
}
4.6、查找单链表倒数第k个节点(https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof)
package com.hr.剑指offer.work1;
/**
* @author zhz
* @date 2020/06/16
**/
public class 面试题22_链表中倒数第k个节点 { /**
* 双指针
* 初始化: 前指针 former 、后指针 latter ,双指针都指向头节点 head 。
* 构建双指针距离: 前指针 former 先向前走 kk 步(结束后,
* 双指针 former 和 latter 间相距 kk 步)。
* 双指针共同移动: 循环中,双指针 former 和 latter 每轮都向前走一步,
* 直至 former 走过链表 尾节点 时跳出(跳出后, latter 与尾节点距离为 k-1,即 latter 指向倒数第 kk 个节点)。
* 返回值: 返回 latter 即可。
*/
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode first=head;
ListNode last=head;
for (int i = 0; i < k; i++) {
first=first.next;
}
while (first!=null){
first=first.next;
last=last.next;
}
return last;
}
}
4.7、合并两个有序的链表(https://leetcode-cn.com/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof)
package com.hr.剑指offer.work1;
/**
* @author zhz
* @date 2020/06/17
* 1、初始化: 伪头节点 dum ,节点 cur 指向 dum 。
* 2、循环合并: 当 l1或 l2为空时跳出;
* 1、当 l1.val<l2.val 时: cur 的后继节点指定为 l1,并 l1向前走一步;
* 2、当 l1.val≥l2.val 时: cur 的后继节点指定为 l2,并 l2向前走一步 ;
* 3、节点 cur 向前走一步,即 cur = cur.nextcur=cur.next 。
* 3、合并剩余尾部: 跳出时有两种情况,即 l1为空 或 l\2为空。
* 1、若 l1 != null : 将 l1添加至节点 cur 之后;
* 2、否则: 将 l2添加至节点 cur 之后。
* 4、返回值: 合并链表在伪头节点 dumdum 之后,因此返回 dum.next即可。
**/
public class 面试题25_合并两个排序的链表 {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dum=new ListNode(0);
ListNode cur=dum;
while (l1!=null&&l2!=null){
if (l1.val<l2.val){
cur.next=l1;
l1= l1.next;
}else{
cur.next=l2;
l2= l2.next;
}
cur=cur.next;
}
cur.next=l1!=null?l1:l2;
return dum.next;
}
}
4.8、从尾到头打印单链表(https://leetcode-cn.com/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof)
package com.hr;
import java.util.Stack;
/**
* 从尾到头打印单链表(使用栈来实现)
*/
public class Test06 {
public static void main(String[] args) {
// 1.创建一个单链表
Node lastNode = new Node(44);
Node node3 = new Node(33, lastNode);
Node node2 = new Node(22, node3);
Node headNode = new Node(11, node2);
// 2.从尾到头打印单链表
reversePrint(headNode);
}
/**
* 从尾到头打印单链表
*利用栈实现
* @param headNode 单链表的首节点
*/
public static void reversePrint(Node headNode) {
// 1.处理 headNode 为 null 的情况
if (headNode == null) {
throw new NullPointerException("headNode 为 null");
}
// 2.创建一个栈
Stack<Node> stack = new Stack<>();
// 3.遍历单链表中的所有节点
Node tempNode = headNode;
while (tempNode != null) {
// 4.把遍历出来的节点添加进入栈中
stack.push(tempNode);
tempNode = tempNode.getNext();
}
// 5.遍历栈中的所有节点
while (!stack.isEmpty()) {
Node node = stack.pop();
System.out.println(node.getData());
}
}
/**
* 从尾到头打印单链表(使用递归来实现)
* @param headNode 单链表的首节点
*/
public static void reversePrint(Node headNode) {
// 1.判断 headNode 为 null 的情况
if (headNode == null) {
return;
}
// 2.从尾到头打印以 headNode 下一个节点为首节点的链表
reversePrint(headNode.getNext());
// 3.打印输出 headNode 中的数据值
System.out.println(headNode.getData());
}
}
4.9、判断单链表是否有环(https://leetcode-cn.com/problems/linked-list-cycle/)
package com.hr;
/**
* 判断单链表是否有环
*/
public class Test08 {
public static void main(String[] args) {
// 1.创建一个单链表
Node lastNode = new Node(55);
Node node4 = new Node(44, lastNode);
Node node3 = new Node(33, node4);
Node node2 = new Node(22, node3);
Node headNode = new Node(11, node2);
lastNode.setNext(node2);
// 2.判断单链表是否有环
boolean flag = hasCycle(headNode);
System.out.println(flag);
}
/**
* 判断单链表是否有环
*
* @param headNode 单链表的首节点
* @return 如果单链表有环,则返回 true,否则返回 fasle
*/
public static boolean hasCycle(Node headNode) {
// 1.处理 headNode 为 null 的情况
if (headNode == null) {
return false;
}
// 2.定义一个快指针,每次往后走两步
Node fast = headNode;
// 3.定义一个慢指针,每次往后走一步
Node slow = headNode;
// 4.定义一个循环,用于判断单链表是否有环
while (fast != null && fast.getNext() != null) {
// 5.设置快慢指针每次往后移动
fast = fast.getNext().getNext();
slow = slow.getNext();
// 6.如果 fast 和 slow 指向的是同一个节点,则证明单链表有环
if (fast == slow) {
return true;
}
}
// 7.执行到此处,证明单链表是无环单链表
return false;
}
}
4.10、从有环链表中,获得环的长度
package com.hr;
/**
* 从有环链表中,获得环的长度
*/
public class Test09 {
public static void main(String[] args) {
// 1.创建一个单链表
Node lastNode = new Node(66);
Node node5 = new Node(55, lastNode);
Node node4 = new Node(44, node5);
Node node3 = new Node(33, node4);
Node node2 = new Node(22, node3);
Node headNode = new Node(11, node2);
lastNode.setNext(node2);
// 2.从有环链表中,获得环的长度
int size = getCycleLength(headNode);
System.out.println(size);
}
/**
* 从有环链表中,获得环的长度
*
* @param headNode 单链表的首节点
* @return 如果单链表有环,则返回环中节点的个数,否则返回 0
*/
public static int getCycleLength(Node headNode) {
// 1.获得快慢指针相交的节点
Node meetNode = meetNode(headNode);
// 2.处理 meetNode 为 null 的情况
if (meetNode == null) {
return 0;
}
// 3.定义一个变量,用于保存环中节点的个数
int size = 0;
// 4.从 meetNode 节点开始,遍历环中的节点
// 4.1 定义一个零时节点,用于辅助单链表的遍历
Node tempNode = meetNode;
// 4.2 定义一个死循环,用于遍历环中的所有节点
while (true) {
// 4.3 让 tempNode 指向它的下一个节点
tempNode = tempNode.getNext();
// 4.4 更新 size 的值
size++;
// 5.如果 tempNode 和 meetNode 指向的是同一个节点,那么就需要停止遍历操作
if (tempNode == meetNode) {
break;
}
}
// 6.返回环中节点的个数
return size;
}
/**
* 获得快慢指针相交的节点
*
* @param headNode 单链表的首节点
* @return 如果单链表有环,则返回快慢指针相交的节点,否则返回 null。
*/
public static Node meetNode(Node headNode) {
// 1.处理 headNode 为 null 的情况
if (headNode == null) {
return null;
}
// 2.定义一个快指针,每次往后走两步
Node fast = headNode;
// 3.定义一个慢指针,每次往后走一步
Node slow = headNode;
// 4.定义一个循环,用于判断单链表是否有环
while (fast != null && fast.getNext() != null) {
// 5.设置快慢指针每次往后移动
fast = fast.getNext().getNext();
slow = slow.getNext();
// 6.如果 fast 和 slow 指向的是同一个节点,则证明单链表有环
if (fast == slow) {
return fast;
}
}
// 7.执行到此处,证明单链表是无环单链表
return null;
}
}
4.11、单链表中,取出环的起始点(https://leetcode-cn.com/problems/linked-list-cycle-ii/)
package com.hr;
/**
* 单链表中,取出环的起始点
*/
public class Test10 {
public static void main(String[] args) {
// 1.创建一个单链表
Node lastNode = new Node(66);
Node node5 = new Node(55, lastNode);
Node node4 = new Node(44, node5);
Node node3 = new Node(33, node4);
Node node2 = new Node(22, node3);
Node headNode = new Node(11, node2);
lastNode.setNext(node2);
// 2.单链表中,取出环的起始点
Node startNode = getStartNode(headNode);
System.out.println(startNode.getData());
}
/**
* 单链表中,取出环的起始点
*
* @param headNode 单链表中的首节点
* @return 返回带环单链表中环的起始点
*/
public static Node getStartNode(Node headNode) {
// 1.获得带环单链表中环的长度
int length = getCycleLength(headNode);
// 2.处理 length 为 0 的情况,也就是处理单链表没有环的情况
if (length == 0) {
return null;
}
// 3.定义 first 和 second 指针,并且设置初始值为单链表的首节点
Node first = headNode, second = headNode;
// 4.让 first 指针往后移动 length 次
for (int i = 0; i < length; i++) {
first = first.getNext();
}
// 5.定义一个循环,用于获得带环单链表中环的起始点
while (first != second) {
// 6.设置 first 和 second 每次往后移动一步
first = first.getNext();
second = second.getNext();
}
// 6.返回带环单链表中环的起始点
return first;
}
/**
* 从有环链表中,获得环的长度
*
* @param headNode 单链表的首节点
* @return 如果单链表有环,则返回环中节点的个数,否则返回 0
*/
public static int getCycleLength(Node headNode) {
// 1.获得快慢指针相交的节点
Node meetNode = meetNode(headNode);
// 2.处理 meetNode 为 null 的情况
if (meetNode == null) {
return 0;
}
// 3.定义一个变量,用于保存环中节点的个数
int size = 0;
// 4.从 meetNode 节点开始,遍历环中的节点
// 4.1 定义一个零时节点,用于辅助单链表的遍历
Node tempNode = meetNode;
// 4.2 定义一个死循环,用于遍历环中的所有节点
while (true) {
// 4.3 让 tempNode 指向它的下一个节点
tempNode = tempNode.getNext();
// 4.4 更新 size 的值
size++;
// 5.如果 tempNode 和 meetNode 指向的是同一个节点,那么就需要停止遍历操作
if (tempNode == meetNode) {
break;
}
}
// 6.返回环中节点的个数
return size;
}
/**
* 获得快慢指针相交的节点
*
* @param headNode 单链表的首节点
* @return 如果单链表有环,则返回快慢指针相交的节点,否则返回 null。
*/
public static Node meetNode(Node headNode) {
// 1.处理 headNode 为 null 的情况
if (headNode == null) {
return null;
}
// 2.定义一个快指针,每次往后走两步
Node fast = headNode;
// 3.定义一个慢指针,每次往后走一步
Node slow = headNode;
// 4.定义一个循环,用于判断单链表是否有环
while (fast != null && fast.getNext() != null) {
// 5.设置快慢指针每次往后移动
fast = fast.getNext().getNext();
slow = slow.getNext();
// 6.如果 fast 和 slow 指向的是同一个节点,则证明单链表有环
if (fast == slow) {
return fast;
}
}
// 7.执行到此处,证明单链表是无环单链表
return null;
}
}
4.12、判断两个单链表相交的第一个交点(https://leetcode-cn.com/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/)
package com.hr;
/**
* 获得两个单链表相交的第一个交点
*/
public class Test11 {
public static void main(String[] args) {
// 1.创建两个单链表
Node lastNode = new Node(77);
Node node6 = new Node(66, lastNode);
Node node3 = new Node(33, node6);
Node node2 = new Node(22, node3);
Node head1 = new Node(11, node2);
Node node5 = new Node(55, node6);
Node head2 = new Node(44, node5);
// 2.获得两个单链表相交的第一个交点
Node commonNode = getFirstCommonNode(head1, head2);
System.out.println(commonNode.getData());
}
/**
* 获得两个单链表相交的第一个交点
* 49
*
* @param head1 单链表 1 的首节点
* @param head2 单链表 2 的首节点
* @return 返回两个单链表相交的第一个交点
*/
public static Node getFirstCommonNode(Node head1, Node head2) {
// 1.处理 head1 或 head2 为 null 的情况
if (head1 == null || head2 == null) {
return null;
}
// 2.获得以 head1 为首节点的单链表长度
int length1 = getLength(head1);
// 3.获得以 head2 为首节点的单链表长度
int length2 = getLength(head2);
// 4.定义 longNode 指针,用于指向长度较长单链表的首节点
Node longNode = length1 > length2 ? head1 : head2;
// 5.定义 shortNode 指针,用于指向长度较短单链表的首节点
Node shortNode = length1 > length2 ? head2 : head1;
// 6.让 longNode 指针往后移动 Math.abs(length1-length2)次
for (int i = 0; i < Math.abs(length1 - length2); i++) {
longNode = longNode.getNext();
}
// 7.定义一个循环,每次让 longNode 和 shortNode 往后移动一次
while (longNode != shortNode) {
longNode = longNode.getNext();
shortNode = shortNode.getNext();
}
// 8.返回两个单链表相交的第一个交点
return longNode;
}
/**
* 获得单链表的长度
*
* @param headNode 单链表的首节点
* @return 返回单链表的长度
*/
public static int getLength(Node headNode) {
// 1.定义一个变量,用于保存单链表的长度
int length = 0;
// 2.定义一个循环,用于实现单链表的遍历操作
Node tempNode = headNode;
while (tempNode != null) {
tempNode = tempNode.getNext();
// 3.更新 length 的值
length++;
}
// 4.返回单链表的长度
return length;
}
}
4.13、复杂链表的复制(https://leetcode-cn.com/problems/fu-za-lian-biao-de-fu-zhi-lcof/)
package com.hr;
/**
* 复杂链表的复制
*/
public class Test12 {
public static void main(String[] args) {
// 1.创建一个复杂链表
Node lastNode = new Node("E");
Node node4 = new Node("D", lastNode);
Node node3 = new Node("C", node4);
Node node2 = new Node("B", node3);
Node headNode = new Node("A", node2);
headNode.random = node3;
node2.random = lastNode;
node4.random = node2;
// 2.复杂链表的复制
Node cloneNode = cloneNode(headNode);
System.out.println();
}
/**
* 复杂链表的复制
*
* @param headNode 复杂链表的首节点
* @return 返回复制后复杂链表的首节点
*/
public static Node cloneNode(Node headNode) {
// 1.处理 headNode 为 null 的情况
if (headNode == null) {
return null;
}
// 2.复制复杂链表中的每一个节点,并且把复制的节点添加到被复制节点的后面
Node curNode = headNode;
while (curNode != null) {
// 创建一个复制节点(N')
Node node = new Node(curNode.data);
// 设置 node 节点的 next 值为 curNode 的后一个节点
node.next = curNode.next;
// 设置 curNode 的 next 值为 node
curNode.next = node;
// 更新 curNode 的值
curNode = node.next;
}
// 3.设置每一个复制节点的 random 指针
curNode = headNode;
while (curNode != null) {
// 获得复制节点(N')
Node node = curNode.next;
// 处理 curNode 的 random 指针不为 null 的情况
if (curNode.random != null) {
// 设置 node 的 random 值为 curNode.random 的下一个节点
node.random = curNode.random.next;
}
// 更新 curNode 的值
curNode = node.next;
}
// 4.把长链表拆分为两个链表
// 定义一个节点,用于保存拷贝后复杂链表的首节点
Node head = headNode.next;
curNode = headNode;
while (curNode != null) {
// 获得复制节点(N')
Node node = curNode.next;
// 设置 curNode 的 next 值为 node.next
curNode.next = node.next;
// 设置 node 的 next 值为 node.next.next
node.next = node.next == null ? null : node.next.next;
// 更新 curNode 的值
curNode = curNode.next;
}
// 5.返回拷贝后复杂链表的首节点
return head;
}
/**
* 节点类
*/
private static class Node {
/**
* 用于保存节点中的数据
*/
private Object data;
/**
* 用于保存指向下一个节点的地址值
*/
private Node next;
/**
* 用于保存指向任意节点的地址值
*/
private Node random;
/**
* 专门为 data 做初始化工作
*
* @param data
*/
public Node(Object data) {
53
this.data = data;
}
/**
* 专门为 data 和 next 做初始化工作
*
* @param data
* @param next
*/
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
}
}
我是小白弟弟,一个在互联网行业的小白,立志成为一名架构师
https://blog.csdn.net/zhouhengzhe?t=1