文章目录
- 前言
- 一.什么是链表
- 二.线性表的链表结构
- 2.1链表的初始
- 2.2 链表的分类
- 2.3 单链表
- 2.4 双链表
- 三.java里面的LinkedList API
- 3.1 基础操作
- 3.2 链表的遍历操作
- 四.模拟实现LinkedList的相关操作
- 创建一个链表
- 头插法
- 尾插法
- 任意位置插入,第一个数据节点为0号下标
- 查找是否包含关键字key是否在单链表当中
- 删除第一次出现关键字为key的节点
- 删除所有值为key的节点
- 得到单链表的长度
- 打印链表
- 清空链表
- 五.链表的相关题目
- 1. 删除链表中等于给定值 val 的所有节点。[OJ链接](https://leetcode.cn/problems/remove-linked-list-elements/description/)
- 2. 反转一个单链表[ OJ链接](https://leetcode.cn/problems/reverse-linked-list/description/)
- 3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。[OJ链接](https://leetcode.cn/problems/middle-of-the-linked-list/description/)
- 4. 输入一个链表,输出该链表中倒数第k个结点。 OJ链接.
- 5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。[OJ链接](https://leetcode.cn/problems/merge-two-sorted-lists/description/)
- 6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。[OJ链接](https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId=8&&tqId=11004&rp=2&ru=/activity/oj&qru=/ta/cracking-the-coding-interview/question-ranking)
- 7. 链表的回文结构。
- 8. 输入两个链表,找出它们的第一个公共结点[OJ链接](https://leetcode.cn/problems/intersection-of-two-linked-lists/)
- 9. 给定一个链表,判断链表中是否有环。[OJ链接](https://leetcode.cn/problems/linked-list-cycle/description/)
- 10.给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
前言
上节课已经说明了线性表的顺序结构,这节课我们将继续了解线性结构中链式结构,大家一定很期待吧,快来加入,跟我一起学习吧.
一.什么是链表
链表由一组节点组成,每个节点包含两部分:本节点的数据域和指向下一个节点的指针(或链)。通过这组节点和指针的关系,形成一个链表。
与数组相比,链表的优点在于插入和删除操作简单,不需要移动大量的数据;缺点是不能直接访问任意位置的节点,需要从起始节点开始顺序遍历。链表可用于实现队列和栈等数据结构。
二.线性表的链表结构
2.1链表的初始
所谓的链表就像火车的一节一节的车厢,如下图所示:
具体的结构就如下图所示:
2.2 链表的分类
单向或者双向
带头后者不带头
循环和非循环
2.3 单链表
这就是一个经典的链式存储结构.
如果说非要用代码表示的话,我暂时用java代码给大家表示一下,一个链表的具体结构长什么样子:
public class MySingleList {
static class ListNode {
public int val;//存储的数据
public ListNode next;//存储下一个节点的地址
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;// 代表当前链表的头节点的引用
}
其实我们还是懂了一点小心思的,我们使用了静态内部类,来构建一个完整的类,这样方便我们创建链表的结构.这里全都是以单链表的例子展开的.
2.4 双链表
public class MyLinkedList {
static class ListNode {
public int val;
public ListNode prev;//前驱
public ListNode next;//后继
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
public ListNode last;
}
三.java里面的LinkedList API
3.1 基础操作
操作 | 描述 | Java实现 |
---|---|---|
boolean add(E e) | 尾插e | list.add(e); |
void add(int index, E element) | 将e 插入到index 位置 | list.add(index, e); |
boolean addAll(Collection<? extends E> c) | 尾插c 中的元素 | list.addAll(c); |
E remove(int index) | 删除index 位置元素 | list.remove(index); |
boolean remove(Object o) | 删除遇到的第一个o | list.remove(o); |
E get(int index) | 获取下标index 位置元素 | list.get(index); |
E set(int index, E element) | 将下标index 位置元素设置为element | list.set(index, element); |
void clear() | 清空 | list.clear(); |
boolean contains(Object o) | 判断o 是否在线性表中 | list.contains(o); |
int indexOf(Object o) | 返回第一个o 所在下标 | list.indexOf(o); |
int lastIndexOf(Object o) | 返回最后一个o 的下标 | list.lastIndexOf(o); |
List<E> subList(int fromIndex, int toIndex) | 截取部分list | list.subList(fromIndex, toIndex); |
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
System.out.println(list.size());
System.out.println(list);
// 在起始位置插入0
list.add(0, 0); // add(index, elem): 在index位置插入元素elem
System.out.println(list);
list.remove(); // remove(): 删除第一个元素,内部调用的是removeFirst()
list.removeFirst(); // removeFirst(): 删除第一个元素
list.removeLast(); // removeLast(): 删除最后元素
list.remove(1); // remove(index): 删除index位置的元素
System.out.println(list);
// contains(elem): 检测elem元素是否存在,如果存在返回true,否则返回false
if (!list.contains(1)) {
list.add(0, 1);
}
list.add(1);
System.out.println(list);
System.out.println(list.indexOf(1)); // indexOf(elem): 从前往后找到第一个elem的位置
System.out.println(list.lastIndexOf(1)); // lastIndexOf(elem): 从后往前找第一个1的位置
int elem = list.get(0); // get(index): 获取指定位置元素
list.set(0, 100); // set(index, elem): 将index位置的元素设置为elem
System.out.println(list);
// subList(from, to): 用list中[from, to)之间的元素构造一个新的LinkedList返回
List<Integer> copy = list.subList(0, 3);
System.out.println(list);
System.out.println(copy);
list.clear(); // 将list中元素清空
System.out.println(list.size());
}
3.2 链表的遍历操作
分为俩种
foreach遍历
迭代器遍历
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
System.out.println(list.size());
// foreach遍历
for (int e : list) {
System.out.print(e + " ");
}
System.out.println();
// 使用迭代器遍历---正向遍历
ListIterator<Integer> it = list.listIterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
System.out.println();
// 使用反向迭代器---反向遍历
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()) {
System.out.print(rit.previous() + " ");
}
System.out.println();
}
四.模拟实现LinkedList的相关操作
我们使用过LinkedList之后,大家一定很好奇它是怎么实现的吧,接下来我们来看看它究竟是怎么一回事.
注意:我们这里全都是以单链表的实现为例子的,
创建一个链表
创建一个链表,我们是以单链表为例子的.
public class MySingleList {
static class ListNode {
public int val;//存储的数据
public ListNode next;//存储下一个节点的地址
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;// 代表当前链表的头节点的引用
}
创建链表的方法,我们选择手动创建
public void createLink() {
ListNode listNode1 = new ListNode(12);
ListNode listNode2 = new ListNode(45);
ListNode listNode3 = new ListNode(23);
ListNode listNode4 = new ListNode(90);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4; /* */
head = listNode1;
}
头插法
头插法的意思顾名思义就是只能从头部插入.
具体的过程就是如下图所示:
代码展示:
public void addFirst(int data){
ListNode listNode = new ListNode(data);
listNode.next = head;
head = listNode;
}
尾插法
尾插法是分为俩种情况的,第一种是没有节点,我们直接可以让头结点指向即可.第二种是有节点了,我们需要定义一个节点,让它执行到最后一个节点,让最后一个节点指向新节点.
代码如下:
//尾插法 O(N) 找尾巴的过程
public void addLast(int data){
ListNode listNode = new ListNode(data);
if(head == null) {
head = listNode;
return;
}
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = listNode;
}
任意位置插入,第一个数据节点为0号下标
这里其实是有讲究的,我们需要找到插入的前一个节点,在进行插入操作.
这里就有俩个步骤了.
1.找到前一个节点
2.进行插入.
具体图示如下:
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data)
throws ListIndexOutOfException{
checkIndex(index);
if(index == 0) {
addFirst(data);
return;
}
if(index == size()) {
addLast(data);
return;
}
ListNode cur = findIndexSubOne(index);
ListNode listNode = new ListNode(data);
listNode.next = cur.next;
cur.next = listNode;
}
private ListNode findIndexSubOne(int index) {
ListNode cur = head;
int count = 0;
while (count != index-1) {
cur = cur.next;
count++;
}
return cur;
}
private void checkIndex(int index) throws ListIndexOutOfException{
if(index < 0 || index > size()) {
throw new ListIndexOutOfException("index位置不合法");
}
}
查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur = head;
while (cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
删除第一次出现关键字为key的节点
删除第一次出现关键词为key的节点,实际上是分为俩个步骤的.
1.查找到要删除的数的前一个节点
2.根据前一个节点找到删除的数
3.删除这个数.
图示如下:
代码如下:
public void remove(int key){
if(head == null) {
return ;//一个节点都没有
}
if(head.val == key) {
head = head.next;
return;
}
ListNode cur = searchPrev(key);
if(cur == null) {
return;
}
ListNode del = cur.next;//要删除的节点
cur.next = del.next;
}
private ListNode findIndexSubOne(int index) {
ListNode cur = head;
int count = 0;
while (count != index-1) {
cur = cur.next;
count++;
}
return cur;
}
删除所有值为key的节点
这个删除所有节点的操作,其实就是想发设发遍历整个链表然后进行删除操作.
1.查找到要删除的数的前一个节点
2.根据前一个节点找到删除的数
3.删除这个数.
4.循环这个操作,直到遍历完整个链表
代码如下:
public void removeAllKey(int key){
if(head == null) {
return;
}
/*while(head.val == key) {
head = head.next;
}*/
ListNode prev = head;
ListNode cur = head.next;
while (cur != null) {
if(cur.val == key) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
if(head.val == key) {
head = head.next;
}
}
得到单链表的长度
这个就是遍历链表的操作,得到最终的长度,我这里就直接给出代码了
//得到单链表的长度 O(N)
public int size(){
int count = 0;
ListNode cur = head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
打印链表
遍历链表依次进行打印即可.
public void display() {
//如果说 把整个链表 遍历完成 那么 就需要 head == null
// 如果说 你遍历到链表的尾巴 head.next == null
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
清空链表
因为有头结点,我把头结点置为空即可.
public void clear() {
head = null;
}
五.链表的相关题目
1. 删除链表中等于给定值 val 的所有节点。OJ链接
这里就不做过多赘述了,这跟我们前面删除定值的val是一样的操作.
class Solution {
public ListNode removeElements(ListNode head, int val) {
//删除值相同的头结点后,可能新的头结点也值相等,用循环解决
while(head!=null&&head.val==val){
head=head.next;
}
if(head==null)
return head;
ListNode prev=head;
//确保当前结点后还有结点
while(prev.next!=null){
if(prev.next.val==val){
prev.next=prev.next.next;
}else{
prev=prev.next;
}
}
return head;
}
}
2. 反转一个单链表 OJ链接
这道题十分有意思,我们给出的思路是用头插法进行链表的翻转,具体的图示如下:
具体代码:
public ListNode reverseList() {
if(head == null) {
return null;
}
//说明 只有一个节点
if(head.next == null) {
return head;
}
ListNode cur = head.next;
head.next = null;
while (cur != null) {
ListNode curNext = cur.next;
//头插法 插入cur
cur.next = head;
head = cur;
cur = curNext;
}
return head;
}
3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ链接
首先简单的说一下这道题的思路,我们要返回中间节点,这个链表其实是有俩种情况的,第一种就是链表为偶数的时候,另外一种就是链表为奇数的时候.
我这里用图示给大家演示一下:
另外我们怎么解决这个问题找中间节点的问题呢?我们引入了快慢指针的概念.大家只要记住,快慢指针是一种算法技巧,使用两个指针遍历链表或数组,一个快指针和一个慢指针。它们以不同的速度移动,快指针每次移动两个节点,慢指针每次移动一个节点。通过这两个指针,可以实现对链表的多种操作。
下面我们就来解决这个问题:
以上就是我门先让fast先走两步,然后再让slow走一步,走到fast为null或者fast.next为null的时候结束.这样奇数个和偶数个都找到了中间节点.
代码如下:
public ListNode middleNode() {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
4. 输入一个链表,输出该链表中倒数第k个结点。 OJ链接.
OJ链接
这个问题我们还是能用快慢指针解决,你们看我接下来的操作就知道了
1.先让fast走k-1步
2. fast走完之后和slow开始一步一步走
3.当fast.next为空的时候,slow所指的位置就是倒数第K个
具体操作如下图所示:
代码如下:
public ListNode findKthToTail(int k) {
if(k <= 0 || head == null) {
return null;
}
ListNode fast = head;
ListNode slow = head;
//1. fast走k-1步
while (k-1 != 0) {
fast = fast.next;
if(fast == null) {
return null;
}
k--;
}
//2、3、
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。OJ链接
这道题究竟要我们干什么呢?大家可以看看下面的图片
我们看到问题就是合并两个有序链表,关键是构建一个新的有序链表,这里的难点是如何同时遍历两个链表。
我给出以下的思路:
- 定义两个指针list1、list2分别指向两个链表的头节点head1和head2。
- 比较list1和list2指向的节点值,将值较小的节点接到结果链表上,并将相应的指针往后移一节点。
- 重复步骤2,直到list1t或者list2到达链表尾部。
- 将未到达尾部的链表直接接到结果链表尾部。
- 返回结果链表的头节点。
具体的图解如下:
重复这个步骤即可.
具体的代码操作如下:
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newHead=new ListNode (0);
ListNode tmp=newHead;
while(list1 != null && list2 !=null){
if(list1.val <list2.val){
tmp.next=list1;
list1=list1.next;
tmp=tmp.next;
}else{
tmp.next=list2;
list2=list2.next;
tmp=tmp.next;
}
}
//循环走完以后,会出现俩种情况
if(list1 != null){
tmp.next=list1;
}
if(list2 !=null){
tmp.next=list2;
}
return newHead.next;
}
6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。OJ链接
具体思路:
- 定义两个链表bs和as,用于存放值小于x和大于等于x的节点。初始时bs和as都指向null。
- 遍历原链表,当节点值小于x时,将该节点添加到bs链表尾部。当节点值大于等于x时,将该节点添加到as链表尾部。
- 遍历结束后,bs链表代表值小于x的节点,as链表代表值大于等于x的节点。
- 判断bs链表和as链表是否同时不为空。如果bs链表为空,直接返回as链表头部。如果as链表为空,直接返回bs链表头部。
- 如果bs链表和as链表同时不为空,则将bs链表尾部节点的next指针指向as链表头部。
- 如果as链表不为空,则将as链表尾部节点的next指针指向null。
- 返回bs链表头部作为结果。
具体代码:
public ListNode partition( int x) {
// write code here
ListNode bs = null;
ListNode be = null;
ListNode as = null;
ListNode ae = null;
ListNode cur = head;
while (cur != null) {
if(cur.val < x) {
if(bs == null) {
bs = cur;
be = cur;
}else {
be.next = cur;
be = be.next;
}
}else {
if(as == null) {
as = cur;
ae = cur;
}else {
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
}
// 有可能不会同时存在小于x 和 大于等于x 的数据
if(bs == null) {
return as;
}
//第一段不为空
be.next = as;
//第2个段为空不为空的问题
if(as != null) {
ae.next = null;
}
return bs;
}
7. 链表的回文结构。
判断链表的回文结构,这个题目的思路也很明确,比如我们判断数字的回文结构是怎么判断的,现在只是换到了链表而已.OJ链接
思路如下:
- 找到链表的中点,可以使用快慢指针。快指针移动两步,慢指针移动一步,当快指针移动到链表尾部时,慢指针正好在中点。
- 反转链表的后半部分。可以从慢指针开始,注意记录前一个节点和后一个节点。
- 比较链表前半部分和后半部分的值。前半部分从头节点开始,后半部分从慢指针开始。
- 如果全部比较相等,则链表为回文结构。否则不是。
- 恢复链表的结构,需要反转后半部分链表。
代码如下:
public boolean chkPalindrome() {
if(head == null) {
return false;
}
if(head.next == null) {
return true;
}
ListNode fast = head;
ListNode slow = head;
//1、找中间节点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//2、 翻转
ListNode cur = slow.next;//代表当前需要翻转的节点
while (cur != null) {
ListNode curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
//3、一个从头往后 一个从后往前
while (slow != head) {
if(head.val != slow.val) {
return false;
}
//偶数的情况
if(head.next == slow) {
return true;
}
slow = slow.next;
head = head.next;
}
return true;
}
8. 输入两个链表,找出它们的第一个公共结点OJ链接
这里实际的情况就是如下图所示:
找俩个单链表节点的相交值
1.我们可以定义俩个指针,来指向俩个链表,
2.比较俩个链表的大小
3.让长链表先走k步.
4,然后再一起走,如果相遇,就是相交链表
具体代码:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
/*
找俩个单链表节点的相交值
我们可以定义俩个指针,来指向俩个链表,
让长链表先走,然后再一起走。
问题来了,我们就能知道相遇点了
*/
//首先我们去解决,哪一个更长的问题
int lenA=0;
int lenB=0;
ListNode pl=headA;
ListNode ps=headB;
while(pl !=null){
lenA++;
pl=pl.next;
}
while(ps !=null){
lenB++;
ps=ps.next;
}
pl=headA;
ps=headB;
int len =lenA-lenB;
if(len <0 ){
pl=headB;
ps=headA;
len=lenB -lenA;
}
//然后就知道,那个长,那个短了
while(len !=0){
pl=pl.next;
len--;
}
while(pl != ps){
pl=pl.next;
ps=ps.next;
}
if (pl==ps && pl==null){
return null;
}
return pl;
}
}
9. 给定一个链表,判断链表中是否有环。OJ链接
具体思路:
- 使用快慢指针,快指针每次走两步,慢指针每次走一步。
- 如果快指针最终走到了链表尾部,说明链表无环,返回false。
- 否则快慢指针最终会相遇在环内,返回true。
为什么我们会使用快慢指针呢?是因为实质上就是我们小学时候使用的追击问题解题思路.我下面将展示一下我的思路.
具体代码
public boolean hasCycle() {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
return true;
}
}
return false;
}
10.给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
OJ链接
让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。
具体思路:
- 先判断链表是否有环,使用快慢指针可以判断。如果无环,直接返回null。
- 如果有环,则将快慢指针重新置于链表头部。慢指针移动一步,快指针移动两步。
- 快慢指针会再次相遇,相遇点就是环入口,返回该节点。
为什么快慢指针能找到环入口?
因为从链表头部到环入口的距离等于相遇点到环入口的距离。快指针速度是慢指针的两倍,所以快指针走的距离是慢指针距离的两倍。
当快慢指针在环内相遇后,快指针已经比慢指针多走了一圈环的距离。所以如果从头节点和相遇点各走相同的距离,最终会在环入口相遇。
我来具体的解释一下我的具体思路:
具体思路:
具体代码:
public ListNode detectCycle() {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
break;
}
}
if(fast == null || fast.next == null) {
return null;
}
//
slow = head;
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}