目录
1. 为什么有链表
1.1 顺序表优缺点
【顺序表的优点】
- 给定下标进行查找时,时间复杂度为O(1)
【顺序表的缺点】
- 插入/删除数据时,必须移动数据,最坏情况是在0下标插入/删除数据,时间复杂度为O(N)
- 扩容有可能浪费内存
1.2 链表
【链表的优点】
- 插入/删除数据时,不必移动数据,时间复杂度为O(1)
- 扩容不会浪费内存
【链表的缺点】查找数据,时间复杂度为O(N)
【总结】顺序表只适合给定下标查找,链表则适合插入/删除,两者正好互补
2. 什么是链表
【概念】
- 链表底层是一个个串起来的节点,其物理(内存)上不连续,逻辑上连续;
- 链表是通过节点进行的连接
【与顺序表的区别】
- 比数组多了一个变量来计数;
- 原因是,按照遍历思路计算数组大小(遇到0终止),若数组中有一个值就是0呢?所以用变量计数,插入一个数据,负责计数的变量就++;
【链表类型】
- 链表类型一共有9种,但只重点掌握两种即可
- 分别是“单向不带头节点的非循环链表(SingleList)”和“双向不带头节点的非循环链表(LinkList)”
【小经验】
- 想要遍历整个链表,while (cur != null)
- 想要使cur停留在最后一个,while(cur.next != null)
3. 实现单向链表CRUD
3.1 题目
【复制到IList接口内】
//头插法
void addFirst(int data);
//尾插法
void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
void addIndex(int index,int data);
//查找是否包含关键字key是否在单链表当中
boolean contains(int key);
//删除第一次出现关键字为key的节点
void remove(int key);
//删除所有值为key的节点
void removeAllKey(int key);
//得到单链表的长度
int size();
//清空链表
void clear();
//打印链表
void display();
3.2 实现SingleList
【前期准备】
- 准备链表的两个域:数据域val和next域,提供val的构造方法
- 准备链表的头节点:head
- 创建链表(用头插/尾插创建链表)
【代码实现】
public class MySingleList implements IList {
//节点内部类定义val和next
static class ListNode{
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
//创建头节点
private ListNode head;
//创建链表
private void createList () {
ListNode node1 = new ListNode(12);
ListNode node2 = new ListNode(23);
ListNode node3 = new ListNode(34);
ListNode node4 = new ListNode(45);
node1.next = node2;
node2.next = node3;
node3.next = node4;
this.head = node1;
}
}
【注】
- 想要遍历整个链表,while (cur != null)
- 想要使cur停留在最后一个,while(cur.next != null)
0. 打印链表
【代码逻辑】
- 找思路:链表是通过每个节点的next域来遍历的,当节点为null时,遍历完成
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:无
【代码实现】
public void display() {
//cur指向头节点,通过next遍历链表
ListNode cur = this.head;
//通过next遍历链表
while (cur !=null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
1. 增(3道)
【需考虑】
- index合法性
- 判断是头插,尾插还是中间插,不同情况分开讨论
1.1 头插法
【画图】
【代码逻辑】
- 找思路:创建节点;节点的next域存head;head指向节点
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:无
【代码实现】
public void addFirst(int data) {
//创建一个节点,传data
ListNode node = new ListNode(data);
//该节点的next域存原来的头节点
node.next = this.head;
//原来的头节点替换成新插入的节点
this.head = node;
}
1.2 尾插法
【画图】
【代码逻辑】
- 找思路:创建节点;通过next域遍历链表到最后一个节点;更改最后一个节点的next域为插入节点的地址
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:无
【代码实现】
public void addLast(int data) {
//创建一个节点,传data
ListNode node = new ListNode(data);
ListNode cur = this.head;
//通过next域遍历链表到最后一个节点
while (cur.next !=null) {
cur = cur.next;
}
//更改最后一个节点的next域为插入节点的地址
cur.next = node;
}
1.3 任意位置插入,第一个数据节点为0号下标
【代码逻辑】
- 找思路:创建节点;获取到index-1下标要走几步;更改index-1的next和节点的next
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:index合法性;分成头插,尾插和中间插;头插和尾插要return
【代码实现】
//获取index-1下标指向的节点
private ListNode getIndex(int index) {
ListNode cur = this.head;
while ( index-1 >0 ) {
cur = cur.next;
index--;
}
return cur;
}
public void addIndex(int index, int data) {
//检查index合法性
if (index < 0 || index > size()) {
throw new RuntimeException();
}
//实例化节点
ListNode node = new ListNode(data);
//获取到index-1下标要走几步
ListNode cur = getIndex(index);
//头插法
if (index == 0) {
addFirst(data);
return; }
//尾插法
if (index == size()) {
addLast(data);
return; }
//中间插
node.next = cur.next;
cur.next = node;
}
2. 查(2道)
2.1 查找是否包含关键字key是否在单链表当中
【代码逻辑】
- 找思路:定义cur从head开始遍历链表;如果cur的val和key相同,return true
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:无
【代码实现】
public boolean contains(int key) {
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
2.2 得到单链表的长度
【代码逻辑】
- 找思路:遍历链表,每次count++,return count
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:无
【代码实现】
public int size() {
ListNode cur = this.head;
int count = 0;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
3. 删(3道)
【需考虑】
- index合法性
- 分开讨论删头节点还是其他节点,不同情况分开讨论
- 若cur.val存的是引用类型,也需要置为null
3.1 删除第一次出现关键字为key的节点
【代码逻辑】
- 找思路:遍历链表,找key;找key的前驱;使key的前驱的next=key后一个节点
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:链表是否为空;分开讨论删头节点和其他节点;前驱合法性(是否为空)
【代码实现】
//找前驱
private ListNode findPrev(int key) {
ListNode cur = this.head;
while (cur != null) {
if (cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;
}
public void remove(int key) {
//链表若为空,删除失败
if (this.head == null) {
return;
}
//删除头节点
if (this.head.val == key) {
this.head = this.head.next;
return; }
//找前驱
ListNode prev = findPrev(key);
//为空,说明没有要删除的节点
if (prev == null) {
throw new RuntimeException();
}
//删除
ListNode cur = prev.next;
prev.next = cur.next;
}
3.2 删除所有值为key的节点
【代码逻辑】
- 找思路:遍历链表;找key;找key的前驱;使key的前驱的next=key后一个节点
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:判断头节点是否为空;分开讨论删头节点和其他节点;
【代码实现】推荐方式二
public void removeAllKey(int key) {
ListNode cur = this.head;
//方式一
ListNode cur = this.head.next;
ListNode prev = this.head;
//遍历链表
while (cur != null) {
//删 除了头节点以外的其他节点
if (cur.val == key) {
prev.next = cur.next;
cur = cur.next;
}else { //cur和prev往下一个节点走
prev = prev.next;
cur = cur.next;
}
//删头结点
if (this.head.val == key) {
this.head = this.head.next;
}
//方式二
//遍历链表
while (cur != null) {
//链表若为空,删除失败
if (this.head == null) {
return;
}
//当节点值为key
if (cur.val == key) {
//删头结点
if (cur == this.head) {
this.head = this.head.next;
}else { //删 除了头节点以外的其他节点
ListNode prev = findPrev(key);
prev.next = cur.next;
}
}
//cur往下一个节点走
cur = cur.next;
}
}
3.3 清空单链表
【代码逻辑】
- 找思路:遍历链表;将链表的每个节点的next域置为null
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:若cur.val存的是引用类型,也需要置为null
【代码实现】
public void clear() {
ListNode cur = this.head;
ListNode curNext = cur.next;
while (cur != null){
//若cur.val存的是引用类型,也需要置为null
//cur.val = null; cur.next = null;
cur = curNext;
}
this.head = null;
}
4. 实现双向链表CRUD
4.1 题目
【复制到IList接口内】
//头插法
void addFirst(int data);
//尾插法
void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
void addIndex(int index,int data);
//查找是否包含关键字key是否在单链表当中
boolean contains(int key);
//删除第一次出现关键字为key的节点
void remove(int key);
//删除所有值为key的节点
void removeAllKey(int key);
//得到单链表的长度
int size();
//清空链表
void clear();
//打印链表
void display();
4.2 实现LinkList
【前期准备】
- 准备链表的三个域:数据域val和next域和prev域,提供val的构造方法
- 准备链表头和尾:head和Last
- 创建链表(用头插/尾插创建链表)
【代码实现】
public class MyLinkList implements IList{
//内部类定义节点,每个节点都有val next prev
static class ListNode {
public int val;
public ListNode next;
public ListNode prev;
public ListNode(int val) {
this.val = val;
}
}
private ListNode head;
private ListNode Last;
}
0. 打印链表
【代码逻辑】
- 找思路:链表是通过每个节点的next域/prev域来遍历的,当节点为null时,遍历完成
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:无
【代码实现】
public void display() {
//cur指向头节点,通过next遍历链表
ListNode cur = this.head;
//通过next遍历链表
while (cur !=null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
1. 增(3道)
【需考虑】
- index合法性
- 判断链表是否为空
- 插入时分开讨论,头插,尾插,中间插
1.1 头插法
【代码逻辑】
- 找思路:创建节点;新节点和头节点衔接;更换头节点
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:判断链表是否为空
【代码实现】
public void addFirst(int data) {
ListNode node = new ListNode(data);
//判断链表是否为空
if (this.head == null) {
//将头节点设为node
this.head = node;
//将尾结点设为node
this.Last = node;
return;
}
//头节点和node连接
node.next = this.head;
//更换头节点
this.head = node;
}
1.2 尾插法
【代码逻辑】
- 找思路:创建节点;尾节点和新节点衔接;更换尾节点
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:无
【代码实现】
public void addLast(int data) {
ListNode node = new ListNode(data);
//如果链表为空
if (this.head == null) {
this.head = node;
this.Last = node;
return;
}
//尾结点和node连接
Last.next = node;
node.prev = Last;
//更换尾结点
Last = Last.next;
}
1.3 任意位置插入,第一个数据节点为0号下标
【代码逻辑】
- 找思路:创建节点;更改node前一个;更改node信息;更改node后一个
- 写代码框架:无
- 填充代码:无
- 完善代码逻辑严谨性:检查index位置合法性;判断链表是否为空;
【代码实现】
//找前驱prev
private ListNode findIndex(int index) {
ListNode cur = this.head;
while(index > 0) {
cur = cur.next;
index--;
}
return cur;
}
//判断链表是否为空
private boolean isEmpty() {
return this.head == null;
}
//检查index位置合法性
private int checkOnAddIndex(int index) {
if (index >= 0 && index <= size()) {
return 0;
}
return -1;
}
public void addIndex(int index, int data) {
//检查index位置合法性
if (checkOnAddIndex(index) == -1) {
System.out.println("位置不合法");
return; }
ListNode node = new ListNode(data);
ListNode in = findIndex(index);
//头插法
if (in == this.head) {
addFirst(data);
}else if (in == this.Last) { //尾插法
addLast(data);
}else { //中间插
//更改node前一个
in.prev.next = node;
//更改node信息
node.next = in;
node.prev = in.prev;
//改in的prev
in.prev = node;
}
}
2. 查(2道)
2.1 查找是否包含关键字key是否在单链表当中
【代码逻辑】
- 找思路:定义cur从head开始遍历链表;如果cur的val和key相同,return true
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:无
【代码实现】
public boolean contains(int key) {
ListNode cur = this.head;
while (cur != null) {
//如果cur.val等于key
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
2.2 得到单链表的长度
【代码逻辑】
- 找思路:遍历链表,每次count++,return count
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:无
【代码实现】
public int size() {
ListNode cur = this.head;
int count = 0;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
3. 删(3道)
【需考虑】
- index合法性
- 分开讨论删头节点还是其他节点,不同情况分开讨论
- 若cur.val存的是引用类型,也需要置为null
3.1 删除第一次出现关键字为key的节点
【代码逻辑】
- 找思路:遍历链表,找key;找key的前驱;使key的前驱的next=key后一个节点
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:链表是否为空;分开讨论删头节点和其他节点;前驱合法性(是否为空)
【代码实现】
public void remove(int key) {
ListNode cur = this.head;
//遍历链表
while (cur != null) {
//该if..else 找 要删除的节点
//要进行删除的节点
if (cur.val == key) {
//该if..else 区分 删头节点或其他节点
//删除的是头节点
if (cur == this.head) {
//头节点往后移动,完成一半的删除操作
this.head = this.head.next;
//该if..else 区分节点数 1个或多个
//如果头节点往后移动后为空,说明链表原来只有一个节点,删除原来唯一的节点后链表为空
if (this.head == null) {
//链表为空,头节点虽然指向为空,但Last还是指向原节点,需要把Last也置为null才算完成删除
this.Last = null;
}else { //如果头节点往后移动后为空,说明链表不只有一个节点
//将新的头节点前驱设为null
this.head.prev = null;
}
}else { //删除 除了头节点以外的其他节点
//删除的不是尾节点时,进行cur前一个节点和后一个节点的衔接
if (cur != this.Last) {
//cur处在尾节点时,cur的下一个为null,会有空指针异常
cur.next.prev = cur.prev;
}else { //删除的是尾节点时,尾节点前移
this.Last = this.Last.prev;
//this.Last.next = null; 等价于下面那行cur.prev.next = cur.next;
}
//cur的前一个的next设为cur的下一个节点
cur.prev.next = cur.next;
}
//因为只删除一次,所以删完要return
return;
}else {
//不是要删除的节点,继续往后走
cur = cur.next;
}
}
}
3.2 删除所有值为key的节点
其与“删除第一次出现关键字为key的节点”不同的是:
- 因为删除许多次,所以删完一次后不用return,继续循环
- 删去最后一个else
【代码逻辑】
- 找思路:遍历链表;找key;找key的前驱;使key的前驱的next=key后一个节点
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:区分删头节点,还是删除了头节点以外的其他节点;区分节点个数,只有一个或多个
【代码实现】
public void removeAllKey(int key) {
ListNode cur = this.head;
//遍历链表
while (cur != null) {
//不要用if..else!!!! 找 要删除的节点
//要进行删除的节点
if (cur.val == key) {
//该if..else 区分 删头节点或其他节点
//删除的是头节点
if (cur == this.head) {
//头节点往后移动,完成一半的删除操作
this.head = this.head.next;
//该if..else 区分节点数 1个或多个
//如果头节点往后移动后为空,说明链表原来只有一个节点,删除原来唯一的节点后链表为空
if (this.head == null) {
//链表为空,头节点虽然指向为空,但Last还是指向原节点,需要把Last也置为null才算完成删除
this.Last = null;
}else { //如果头节点往后移动后为空,说明链表不只有一个节点
//将新的头节点前驱设为null
this.head.prev = null;
}
}else { //删除 除了头节点以外的其他节点
//删除的不是尾节点时,进行cur前一个节点和后一个节点的衔接
if (cur != this.Last) {
//cur处在尾节点时,cur的下一个为null,会有空指针异常
cur.next.prev = cur.prev;
}else { //删除的是尾节点时,尾节点前移
this.Last = this.Last.prev;
//this.Last.next = null; 等价于下面那行cur.prev.next = cur.next;
}
//cur的前一个的next设为cur的下一个节点
cur.prev.next = cur.next;
}
//不同点1:因为删除许多次,所以删完不用return
//return; }
//不同点2:删去else
cur = cur.next;
}
}
3.3 清空单链表
【代码逻辑】
- 找思路:遍历链表;将链表的每个节点的next域和prev域置为null;将头节点和尾节点置为null
- 写代码框架:无
- 填充代码:无
- 完善代码严谨性:若cur.val存的是引用类型,也需要置为null
【代码实现】
public void clear() {
ListNode cur = this.head;
while (cur != null) {
cur.prev = null;
cur.next = null;
cur = cur.next;
//如果引用类型,cur.val也需要置为null
//cur.val = null;
}
this.head = null;
this.Last = null;
}
5. 面试题
提问形式
- ArrayList(顺序表)和LinkList(链表)的区别是什么?
- 数组和链表的区别?
【答】若经常给定下标查询的话,适合用顺序表;若经常进行插入/删除操作的话,用链表