一.什么是链表
什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
如图所示:
链表类型
单链表
刚刚说的就是单链表。
双链表
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
如图所示:
循环链表
循环链表,顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
链表的定义
接下来说一说链表的定义。
链表节点的定义,很多同学在面试的时候都写不好。
这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。
而在面试的时候,一旦要自己手写链表,就写的错漏百出。
// 单链表链表节点
private static class Node<E> {
E val;
Node<E> next;
Node(E val) {
this.val = val;
}
}
二.链表的增删改查的操作
对于链表增删改查操作不熟练的可以看下面的文章;
单链表的基本操作https://blog.csdn.net/weixin_56522854/article/details/133909560?spm=1001.2014.3001.5502
三.沙场秋点兵:上LeetCode
LeetCode21:合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = [] 输出:[]
示例 3:
输入:l1 = [], l2 = [0] 输出:[0]
分析:本道题给两个升序的链表 进行合并成一条升序的大链表 ,解法还是比较简单的,既然是要把两条链表合并成一条链表的操作的话,我们就可以很明显的知道需要使用到辅助链表 那么所谓的辅助链表就是我们所说的 虚拟头结点 因为只要是链表的操作就是用头节点进行操作的.那么我们直接来看代码
其实下面的代码我们可以简单的理解 我们在穿衣服拉拉链的时候的操作 把两个链表看做拉链的两边进行合并 就是我们的while循环里面 我们谁小谁先放到辅助链表里面 但是要注意的是 我们要不断地 让辅助链表不断的前进. 当遇到P1或者P2越界的话 我就让其中一个没有越界剩下的元素 全部接到辅助链表里面去.
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//第一步:创建一个虚拟头节点进行作为 辅助链表来进行操作
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
ListNode p1 = list1;
ListNode p2 = list2;
//第二步:进行对两个升序链表进行像拉拉链一样的操作
while(p1!=null && p2!=null){
if(p1.val>p2.val){
//将小的放入到虚拟链表里面去
p.next = p2;
p2 = p2.next;
}else {
//这个情况就是p1<p2
p.next = p1;
p1 = p1.next;
}
//当跳出while循环的时候 p要继续往下走一步
p = p.next;
}
if(p1!=null){
p.next = p1;
}
if(p2!=null){
p.next = p2;
}
//需要注意的是 要返回虚拟节点的下一个节点才是我们的头结点
return dummy.next;
}
}
LeetCode23:合并K个有序链表
分析其实这道题和上面的题目是有所谓的异曲同工之处的.就是上一题是进行对两个有序链表进行合并成一个大的有序链表 那这边进行对K个链表进行合并成一个有序的链表的话 我们依旧可以进行.但是既然是算法 我们就要考虑到所谓的时间复杂度和空间复杂度
下面的代码就是我们对整个升序链表进行两两 进行比较然后再进行合并成一个大的升序链表
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
//传入的是一个链表的集合
for(ListNode node :lists){
res = mergeTwoLists(res ,node)
}
return res;
}
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//第一步:创建一个虚拟头节点进行作为 辅助链表来进行操作
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
ListNode p1 = list1;
ListNode p2 = list2;
//第二步:进行对两个升序链表进行像拉拉链一样的操作
while(p1!=null && p2!=null){
if(p1.val>p2.val){
//将小的放入到虚拟链表里面去
p.next = p2;
p2 = p2.next;
}else {
//这个情况就是p1<p2
p.next = p1;
p1 = p1.next;
}
//当跳出while循环的时候 p要继续往下走一步
p = p.next;
}
if(p1!=null){
p.next = p1;
}
if(p2!=null){
p.next = p2;
}
//需要注意的是 要返回虚拟节点的下一个节点才是我们的头结点
return dummy.next;
}
}
解法二:最优解,整个在合并成K个有序链表的话 其实难点也是难道我们该如何找到K个链表里面的最小的那一个节点 最为大链表的头节点操作. 那么就要使用到一种叫优先级队列 优先级队列简单来说 你把K个元素放进去 他会将进行自动排序 并且在你每次去取元素的时候 都是取到的是K个元素里面最小的元素.
这边文章就很好的解释了什么是优先级队列 既然我们拿到了K个元素的最小值之后 就可以进行对剩下的元素进行比较操作 然后在插入到我们需要使用的虚拟节点的 辅助链表中 那么就直接来看
了解什么是优先级队列https://blog.csdn.net/l_ppp/article/details/107549160
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
//优先级队列
PriorityQueue<ListNode> pq = new PriorityQueue<>(
lists.length,(a,b)->(a.val-b.val));
//将K个链表的头头结点加入到pq中去
for(ListNode list:lists){
if(list!=null){
pq.add(list);
}
}
//通过优先级队列获取到最小的 头结点
while (!pq.isEmpty()){
ListNode minPoll = pq.poll();
//获取到最小的节点之后 就可以进行比较操作
p.next = minPoll;
if(minPoll.next!=null){
pq.add(minPoll.next);
}
//注意的是要让我们的辅助链表不断像前走
p = p.next;
}
return dummy.next;
}
}
LeetCode160:两个链表是否相交
分析:对于本题我们采用的思路其实是, 我们将两个链表合成两个大的链表进行遍 第一个大链表是 先遍历A小链表在接着后面的B小链表 在去遍历第二个大链表 但是先遍历B小链表在接着遍历A小链表.
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA , p2 = headB;
//这边定义两个节点 进行遍历两个条大链表
while (p1!=p2){
//这边定义p1进行定义先走A-B p2进行定义先走B-A
if(p1==null){
//当p1==null就相当于 A小链表走完了 那么就走B小链表
p1 = headB;
}else {
p1 = p1.next;
}
if(p2==null){
p2 = headA;
}else {
p2 = p2.next;
}
}
return p2;
}
}
当然还以简单的做法 既然是要看两个链表是否存在相同的数据 那么就可以使用到哈希表来进行 判断 使用hashMap/hashSet来进行判断
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//第二种做法 使用hash表
HashSet set = new HashSet();
while (headA!=null){
set.add(headA);
headA = headA.next;
}
//接下里进行判断是否有存在 就是进行遍历B链表
while (headB!=null){
if(set.contains(headB)){
return headB;
}
headB = headB.next;
}
return null;
}
}
LeetCode2:给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外,这两个数都不会以 0 开头。 示例 1: 输入:l1 = [2,4,3], l2 = [5,6,4] 输出:[7,0,8] 解释:342 + 465 = 807.;
分析:本题的难点其实就是在于 关于进位的问题 我们来一起分析仪 : 比如两个链表A[6 7 2] B[ 2 3 4] 这样子的链表 我们对链表进行遍历操作 先取到A的第一个元素 6 再去取到B的第二个元素2 相加等于8 很好这样子不用进行进位操作 那么在while循环里面我们进行继续 遍历链表操作 就到了A的7 加上 B的3 这样子等于10 那么我们怎么办? 用一个sum=10 我们要知道他的进位carry = 1 . 那么sum/10 = 1 那么就知道进位是carry==1 这个时候 对于两个链表的下面两个元素相加 的话 就要进行加一操作了 那么我们就只需要加carry值即可. 但是但是但是 重要的事情说三遍
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
//用于记录进位值
int carry = 0;
while (l1!=null || l2!=null){
int x = l1==null?0:l1.val;
int y = l2==null?0:l2.val;
int sum = x+y+carry;
carry = sum/10;
sum = sum%10;
cur.next = new ListNode(sum);
cur = cur.next;
if(l1!=null){
l1 = l1.next;
}
if(l2!=null){
l2 = l2.next;
}
}
//经过测试如果不写这行代码的话 当链表的最后一位相加大于10的话 就没有进行进位操作
//意思就是如果遍历结束了,还是要进位,就创建一个新的节点,这个节点就是进位的值,也就是1
if(carry==1){
cur.next = new ListNode(carry);
}
return dummy.next;
}
}
LeetCode19删除链表的倒数第K个节点
分析:对于倒数我们都知道不好进行处理 那么我们就进行用正数来进行操作 对于倒数第K个数 正数来其实就是N-K+1个数 (N为链表的长度) 既然我们知道了这一点那么就可以进行来做题了 对于删除节点的话 无非即使把要删除的这个节点的上一个节点指向要删除节点的下一个节点.并且这里还是同样使用了我们所谓的双指针的方式 来进行操作 让指针P1先走K步 那么P1就还剩下N-K步走到链表的尾部 这个时候我们再让 P2节点开始移动 直到P1走到末尾节点的时候 P2就是走到了N-K+1的节点了.
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode();
dummy.next = head;
//要找到倒数第N个节点 就要正数的第N-K+1个节点
ListNode result = removNthFrom(dummy,n+1);
result.next = result.next.next;
return dummy.next;
}
public ListNode removNthFrom(ListNode head,int n){
ListNode p1 = head;
//先让P1走n步
for (int i = 0; i < n; i++) {
p1 = p1.next;
}
//接下来再让 P2 走n-k步
ListNode p2 = head;
while (p1!=null){
p2 = p2.next;
p1 = p1.next;
}
return p2;
}
}