链表是通过指针相连的线性结构 每一个节点由两部分组成 一个是数据域一个是指针域(存放指向下一个节点的指针)最后一个节点的指针域指向null(空指针的意思)
链表的入口节点称为链表的头结点也就是head。
如图所示(代码随想录图示)
链表的类型:
单链表
如上所示
双链表
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
如图所示(代码随想录图示):
循环链表
循环链表 就是链表首尾相连 可以用来解决约瑟夫环 之前考试的时候做过约瑟夫做不出来捏
如图所示(代码随想录图示):
链表的存储方式
链表是通过指针域的指针链接在内存中各个节点 链表中的节点在内存中不是连续分布的 他们散乱分布在内存中的某地址上 分配机制取决于操作系统的内存管理
如图所示(代码随想录图示):
这个链表起始节点为2 终止节点为7 各个节点分布在内存的不同地址空间上 通过指针串联在一起
链表的定义
Java写法:
public class ListNode{
//节点的值
int val;
//下一个节点
ListNode next;
//节点的构造函数(无参)
public ListNode(){
}
//节点的构造函数(有一个参数)
public ListNode(int val){
this.val = val;
}
//节点的构造函数(有两个参数)
public ListNode(int val, ListNode next){
this.val = val;
this.next = next;
}
}
链表的操作
删除节点
删除D节点,如图所示(代码随想录图示):
把c节点的next指针指向e就可以算是删除操作
D节点有可能依然存留在内存里 在c++中要释放内存 但是我用的java会自动释放 所以这可不理会捏
添加节点
如图所示(代码随想录图示):可以看出链表的增添和删除都是O(1)操作 也不会影响到其他节点
但是要注意 要删除第五个节点 需要从头节点查找到第四个节点通过next指针进行删除操作 查找的时间复杂度是O(n)
性能分析
再把链表的特性和数组的特性进行一个对比,如图所示(代码随想录图示):
数组一旦创建就是固定的无法动态改变 如果想改动数组长度则需要重新定义数组 链表的长度可以是不固定的 并且可以动态增删 适合数据量不固定 频繁增删 较少查询的场景 (记得回来复习巩固);
203.移除链表元素
文章讲解/视频讲解:: 代码随想录
要求:理解并熟悉虚拟头结点的概念
Java代码(比较暴力):
class Solution {
public ListNode removeElements(ListNode head, int val) {
//时间复杂度on 空间复杂度o1
//直接使用原来的链表来进行移除节点操作:
while(head != null && head.val == val){
head = head.next;
}
//如果头结点是null 提前退出
if(head == null){
return head;
}
ListNode curr = head;//进行查找删除
while(curr != null && curr.next != null){
if(curr.next.val == val){
curr.next = curr.next.next;
}else{
curr = curr.next;
}
}
return head;
}
}
解法二(**虚拟头结点**):
class Solution {
public ListNode removeElements(ListNode head, int val) {
//设置一个虚拟头结点
ListNode dummy = new ListNode();
dummy.next = head;
ListNode cur = dummy;
while(cur.next != null){
if(cur.next.val == val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return dummy.next;//记得返回的是头结点不是虚拟头结点
}
}
两个解法时间复杂度都是O(n) 空间复杂度O(1)
707.设计链表
用虚拟头结点写更方便!不熟悉 不会写 抄一抄动动脑写一写
Java代码:
class ListNode{
int val;
ListNode next;
ListNode(){}
ListNode(int val){
this.val = val;//初始化
}
}
class MyLinkedList {
//size存储链表元素个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值 注意index是从0开始的 第0个节点就是头结点
public int get(int index) {
//如果index非法 返回-1
if(index < 0 || index >= size){
return -1;
}
ListNode cur = head;
//包含一个虚拟头结点 所以查找第 index + 1 个节点
for(int i = 0; i <= index; i++){
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
ListNode newNode = new ListNode(val);
newNode.next = head.next;
head.next = newNode;
size++;
//在链表最前面插入一个节点 等价于在第0个节点前添加
//addAtIndex(0,val);
}
public void addAtTail(int val) {
ListNode newNode = new ListNode(val);
ListNode cur = head;
while(cur.next != null){
cur = cur.next;
}
cur.next = newNode;
size++;
//在链表最后插入一个节点 等价于在(末尾+1)个元素前添加
//addAtIndex(size, val);
}
//在第 index 节点之前插入一个新节点 例如index为0 那么新插入的节点为链表的新头结点
//如果 index 等于链表的长度 则说明是新插入的节点为链表的尾节点
//如果 index 大于链表的长度 则返回空
public void addAtIndex(int index, int val) {
if(index > size){
return;
}
if(index < 0){
index = 0;
}
size++;
//找到要插入的节点的前驱
ListNode pre = head;
for(int i = 0; i < index; i++){
pre = pre.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pre.next;//联想一下 想想图是怎么理解的
pre.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if(index < 0 || index >= size){
return;
}
size--;
//因为有虚拟头结点 所以不用对index=0的情况进行特殊处理
ListNode pre = head;
for(int i = 0; i < index; i++){
pre = pre.next;
}
pre.next = pre.next.next;
}
}
//双链表
class ListNode{
int val;
ListNode next,prev;
ListNode() {};
ListNode(int val){
this.val = val;
}
}
class MyLinkedList {
//记录链表中元素的数量
int size;
//记录链表的虚拟头结点和尾结点
ListNode head,tail;
public MyLinkedList{
//初始化操作
this.size = 0;
this.head = new ListNode(0);
this.tail = new ListNode(0);
//这一步非常关键 否则在假如头结点的操作中会出现null.next的操作!
head.next = tail;
tail.prev = head;
}
public int get(int index) {
//判断index是否有效
if(index >= size){
return -1;
}
ListNode cur = this.head;
//判断是哪一边遍历时间更短
if(index >= size /2){
//tail开始
cur = tail;
for(int i = 0; i < size - index; i++){
cur = cur.prev;
}else{
for(int i = 0; i <= index; i++){
cur = cur.next;
}
}
}
return cur.val;
}
public void addAtHead(int val) {
//等价于在第0个元素前添加
addAtIndex(0,val);
}
public void addAtTail(int val) {
///等价于在最后一个元素(null) 前添加
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
//index大于链表长度
if(index > size){
return;
}
size++;
//找到前驱
ListNode pre = this.head;
for(int i = 0 ; i < index ; i++){
pre = pre.next;
}
//新建节点
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;//画画图 就回理清楚逻辑
}
public void deleteAtIndex(int index) {
//、判断索引是否有效
if(index >= size){
return;
}
//删除操作
size--;
ListNode pre = this.head;
for(int i = 0 ; i < index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
写的我手抽筋。。但是很好的巩固了单双链表的概念和思考方式 多多复习思考
206.反转链表
反转链表看起来挺经典的 只需要改变链表的next指针的指向 直接将链表反转 而不用重新定义一个新的链表 图解(代码随想录图示):
所以用递归或者双指针都是ok的 双指针写法如下以及图解(代码随想录图示):
Java代码:
class Solution {
public ListNode reverseList(ListNode head) {
//双指针
ListNode pre = null;
ListNode cur = head;
ListNode temp = null;
while(cur != null){
temp = cur.next;//保存下一个节点
//重点
cur.next = pre;//指向下个节点的索引pre 相当于反转方向
//重点
pre = cur;//把当前节点值赋给pre
cur = temp;//给cur变量赋下一个节点 例子 null<-1 2 3 4 5 cur从 1 变 2
}
return pre;
}
}
递归和上述双指针思路一致 写下来自己看看 也是一种新写法!
Java代码:
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur){
if(cur == null){
return prev;
}
ListNode temp = null;
temp = cur.next;//先保存下一个节点
cur.next = prev;//翻转
//更新prev cur位置
//prev = cur
//cur = temp
return reverse(cur,temp);
}
}
打卡完成 爆肝 考完了试就是舒服 今天等会回家看看左神课