文章目录
LinkedList 与链表
链表
链表是一种物理存储结构上非连续存储结构 , 数据元素的逻辑顺序是通过链表中的引用链接次序实现的
链表结构 :
- 单向或者双向
- 带头或者不带头
- 循环或者非循环
主要掌握两种 : 无头单向非循环链表: 结构简单 , 一般不会单独用来存数据 . 实际中更多是作为其他数据结构的子结构
无头双向链表: 在Java的集合框架库中的 LinkedList 底层实现就是无头双向循环链表
链表实现
创建一个链表
无头单向非循环链表实现
static class ListNode {
public int val;//存储的数据
public ListNode next;//存储的下一个节点
public ListNode(int val) {
this.val = val;
}
}
public ListNode head ;//代表当前链表的头结点的引用
public void creatList() {
ListNode listNode1 = new ListNode(11);
ListNode listNode2 = new ListNode(12);
ListNode listNode3 = new ListNode(13);
ListNode listNode4 = new ListNode(14);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
head = listNode1;
}
从指定位置开始打印链表
指定一个 cur 来表示 head , 防止 直接使用head遍历造成第二次遍历head找不到 , 使用 while 循环来遍历链表,当 cur 不为空时 , 将 cur .next 赋值给 cur
public void display() {
//如果说 把整个链表遍历完成 那么就需要 head == null
//如果说 要遍历到链表的尾巴 head.next = null;
ListNode cur = head;
//如果直接用 head 遍历 , 第二遍 遍历的时候会找不到head在哪里
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
查找是否包含关键字key
指定一个 cur 来表示head , 代替head遍历链表 , 当cur 不为空时 , 看 key 的值 是否和cur.val 一致 , 如果不一致 , 将cur.next 赋值给 cur , 继续遍历 , 直到找到key 为止 , 结束循环
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
得到单链表的长度
定义一个循环次数count , 指定cur 来表示 head来遍历链表 , 当 cur 不为空时 , count++ , 将 cur.next 的值给cur , 返回count
public int size() {
int count = 0;
ListNode cur = head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
头插法
定义一个 node , 将头结点的地址 传给 node.next ,再将 node 的值 变为head
public void addFirst(int data) {
ListNode node = new ListNode(data);
node.next = head;
head = node;
}
尾插法
如果 是个空链表 , 将node 的值 直接给head , 返回 . 如果非空 , 定义 cur 来代替head遍历数组 , 当 cue.next 不为null时 , 将 cur.next 赋值给 cur . 当cur 走到链表末尾时 , 即 cur.next = null , 再将node 插入
public void addLast(int data) {
ListNode node = new ListNode(data);
if (head == null) {
head = node;
return;
}
ListNode cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
任意位置插入 , 第一个数据节点为0 下标
如果插入的位置 为头 , 直接调用 头插法
如果 插入位置 为链表的长度 , 调用尾插法
如果插入位置非头非尾 , 首先 , 检查插入位置是否合法 , 如果位置 小于 0 或者 大于链表长度 , 抛出位置不合法的异常 . 如果位置合法 , 找到想要插入位置的前一个位置下标 , 定义cur 来从头部遍历链表 , 定义循环次数 count , 当 count 不等于 index - 1 ,时 , 将 cur.next 给cur , count ++ , 直到 count = 位置减一 , 返回 cur
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 node = new ListNode(data);
node.next = cur.next;
cur.next = node;
}
/**
* 找到index - 1 的数据的位置
*/
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 节点
如果链表为空 , 直接返回
如果要删除的值 等于 头结点的值, 直接让 head 往后走 head = head.next
如果删除的值不等于头结点的值 , 首先找到 要删除 Key 的前一个节点 , 定义 cur , 当cur.next 不为空时 , 如果 要删除的值 , 等于 cur.next.val , 返回 cur , 将cur .next 赋值给 del , 再将 del .next 赋值给 cur .next. 如果不等于 , cur 继续往后走
/**
* 删除第一次出现key 节点
*/
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;
}
/**
* 找到要删除 key的前一个节点
*/
private ListNode searchPrev(int key) {
if(head == null) {
return null;
}
ListNode cur = head;
while (cur.next != null) {
if(cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;//没有你要删除的节点
}
删除所有值为key 的节点
定义一个cur , 再定义一个cur的前驱 prev (小跟班) , 如果cur.val等于key时 , 将cur.next 赋值给 prev.next , cur继续向前遍历
(如果要删除12) 遇到是12 , 将cur.next 复制给 prev.next
如果cur.val 不等于key时 , 将cur赋值给prev ,cur继续遍历
public void removeAllKey(int key) {
if(head == null) {
return;
}
ListNode cur = head.next;
ListNode prev = head;
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;
}
}
清空链表
直接将head置为null
public void clear() {
head = null;
}
反转链表
如果 head 为空时 返回null . 如果只有一个节点 head.next 为null时 , 返回head
定义cur 为head.next . 将head.next 赋值为null(作为链表的尾部). 当cur不为空时 , 定义一个curNext为cur.next . 将每个节点头插法 插入链表 , 最后返回head
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;
}
找中间节点
找出并返回链表的中间节点 , 如果有两个中间节点 , 则返回第二个中间节点
定义快慢指针 , 快指针走两步, ,慢指针走一步 , 路程相同 , 所以当fast 在终点的时候 , slow 在中间位置
public ListNode middleNode() {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {//顺序不能颠倒 , 否则空指针异常 .next的前提是 fast 不为空
fast = fast.next.next;//走两步
slow = slow.next;//走一步
}
return slow;
}
输出链表中倒数第k 个节点
定义快慢指针
- 先让fast 走 k-1 步
- fast 走到 k-1 步之后 , slow开始和 fast 一起走
- 当fast.next 为空时 , slow 所指的位置就是倒数第 k 个节点
比如 : 找倒数第三个节点 , fast 走 3 - 1 = 2 步 , 这是slow开始走 . 走到最后一个节点是 fast.next = null , slow 就是倒数第三个节点
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;
}
合并两个有序链表 , 返回一个升序链表
定义一个虚拟节点newHead , 再定义一个tmp 来代表 newHead 遍历链表
当head1 和 head2 都不为空时 , 如果 head1 < head2 的值. 将head1的值给tmp.next, 然后head1往后走 head1=head1.next. tmp也往后走
else 如果 head1 > head2的值, 将head2 的值给 tmp.next , head2往后走 head2 = head2.next. tmp 也往后走
如果 head1不为空时 , tmp.next = head1
如果 head2不为空时 , tmp.next = head2
最后返回 newHead.next
public static MySingleList.ListNode mergeTwoLists(MySingleList.ListNode head1, MySingleList.ListNode head2) {
MySingleList.ListNode newHead = new MySingleList.ListNode(0);
MySingleList.ListNode tmp = newHead;
while (head1 != null && head2 != null) {
if (head1.val < head2.val) {
tmp.next = head1;
head1 = head1.next;
tmp = tmp.next;
}else {
tmp.next = head2;
head2 = head2.next;
tmp = tmp.next;
}
}
if (head1 != null) {
tmp.next = head1;
}
if (head2 != null) {
tmp.next = head2;
}
return newHead.next;
}
链表的回文
如果 head 为空时 , 返回false . 如果链表中只有head一个节点 , 返回true.
定义一个快慢指针
1.找到链表中的中间节点
快慢指针 当 fast不为空和fast.next不为空时 , 快指针走两步 , 慢指针走一步
2.将中间节点之后的节点反转
定义 cur为 slow.next . 当cur 不为空时 , 定义curNext 为 cur.next . 将 cur.next 的值给 slow . slow往后走 slow = cur. cur也往后走 cur= cur.Next
3.头结点从前往后走 , 尾节点从后往前走
slow从后往前 , head从前往后 . 当slow.val = head.val 则有回文数
偶数:如果head.next == slow 则返回true
奇数:然后slow往前 slow=slow.next .head往后走 .head=head.next
返回true
public boolean chkPalindrome(MySingleList.ListNode head) {
if (head == null) {
return false;
}
if (head.next == null) {
return true;
}
MySingleList.ListNode fast = head;
MySingleList.ListNode slow = head;
//1. 找中间节点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//2.反转
MySingleList.ListNode cur = slow.next;
while (cur != null) {
MySingleList.ListNode curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
//3.从后往前走 , 从前往后走
while (head != slow) {
if (head.val != slow.val) {
return false;
}
//偶数判断
if (head.next == slow) {
return true;
}
slow = slow.next;
head = head.next;
}
return true;
}
LinkedList 的模拟实现
图示:无头双向链表
Linkedlist 需要定义尾节点 last
### 头插法
定义一个 node , 如果头结点 为空时 , 前驱和后继都是 node
不为空时 , 将 node 的后继给head , head 的前驱 给node , 再将head 给 node
public void addFirst(int data){
ListNode node = new ListNode(data);
if (head == null) {
head = node;
last = node;
}else {
node.next = head;
head.prev = node;
head = node;
}
}
尾插法
如果头结点为空 , 将 head 和 last 都给 node (直接插入)
或者 , 将last 的后继给node , node 的前驱给last , 再将node 变为 last
public void addLast(int data){
ListNode node = new ListNode(data);
if (head == null) {
head = node;
last = node;
}else {
last.next = node;
node.prev = last;
last = node;
}
}
任意位置插入
1.当插入位置不合法时 , 抛出异常
2.当插入位置为头结点时 , 采用头插法 . 当插入位置为尾节点时 , 采用尾插法
3.定义 cur 表示要插入的位置 遍历链表 , 链表中间插入元素 , 需要更改四个位置
1)将插入元素 node 的后继改为cur
2)将cur 前一个节点的后继改为 node
3)将 node 的前驱改为 cur 的前驱
4)将 cur 的前驱改为 node
public void addIndex(int index,int data){
if(index < 0 || index > size()) {
throw new ListIndexOutOfException();
}
if(index == 0) {
addFirst(data);
return;
}
if(index == size()) {
addLast(data);
return;
}
ListNode cur = findIndex(index);
//
ListNode node = new ListNode(data);
node.next = cur;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
查看是否包含关键字 key
定义 cur 从 head 开始遍历链表 , 当 cur 不为空时 , 如果 cur 的值 等于 key , 返回 true , 否则 cur 继续遍历 , 直到找到为止 , 如果没有这可关键字 , 返回false
public boolean contains(int key){
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
删除第一次出现的关键字为key 的节点
1.定义 cur 从头开始遍历
2.如果 cur 的值不等于要删除的关键字
如果要删除的关键字为头结点 : 将 head 向后移一位 ,
如果只有一个头结点 , 那就是 头结点不为空 , 将 head 的前驱置为空
或者删除的是中间节点或者尾巴节点 : 将 cur 的前一个节点的后继置为 cur 的后继
中间节点: 如果 cur的后继不为空 , 将 cur 后继的前驱置为 cur 的前驱
尾巴节点: 将 last 置为last的前驱
表结构:
1.删除头结点
2)删除中间节点
删除尾节点:
public void remove(int key){
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
//1.删除的是头结点
if (cur == head) {
head = head.next;
if (head != null) {
head.prev = null;
}
}else {
//删除的是 中间节点或者尾巴节点
cur.prev.next = cur.next;
//中间节点
if (cur.next != null) {
cur.next.prev = cur.prev;
}else {
//尾巴节点
last = last.prev;
}
}
return;
}
cur = cur.next;
}
}
删除所有值为 key 的节点
同上 , 删除上述代码的 return , 删完一次关键字后继续进行删除操作
public void removeAllKey(int key){
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
//1.删除的是头结点
if (cur == head) {
head = head.next;
if (head != null) {
head.prev = null;
}
}else {
//删除的是 中间节点或者尾巴节点
cur.prev.next = cur.next;
//中间节点
if (cur.next != null) {
cur.next.prev = cur.prev;
}else {
//尾巴节点
}
}
}
cur = cur.next;
}
}
得到单链表的长度
public int size(){
ListNode cur = head;
int len = 0;
while (cur != null){
len++;
cur = cur.next;
}
return len;
}
置空链表
public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode curNext = cur.next;
cur.next = null;
cur.prev = null;
cur = curNext;
}
head = null;
last = null;
}
LinkedList 说明
LinkedList 的底层是双向链表 , 由于链表没有将元素存储在连续的空间中 , 元素存储在单独的节点中 , 然后通过引用将节点连接起来 , 因此在任意位置插入或者删除元素是 , 不需要搬移元素 , 效率较高
LinkedList 实现的接口
- LinkedList 实现了 List 接口
- LinkedList 的底层使用了双向链表
- LinkedList 没有实现RandomAccess 接口 , 因此LinkedList 不支持随机访问
- LinkedList 的任意位置插入和删除元素时效率比较高 , 时间复杂度为 O(1)
- LinkedList 比较适合任意位置插入的场景
LinkedList 的构造
public static void main(String[] args) {
// 构造一个空的LinkedList
List<Integer> list1 = new LinkedList<>();
List<String> list2 = new java.util.ArrayList<>();
list2.add("JavaSE");
list2.add("JavaWeb");
list2.add("JavaEE");
// 使用ArrayList构造LinkedList
List<String> list3 = new LinkedList<>(list2);
}
ArrayList 和 LinkedList 的区别
不同 | ArrayList | LinkedList |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续, 但物理上不一定连续 |
随机访问 | 支持 O(1) | 不支持 😮(n) |
头插 | 需要搬移元素 , 效率低O(n) | 只需要修改引用的指向 , 时间复杂度为O(1) |
插入 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储 + 频繁访问 | 任意位置插入和删除频繁 |
33