java数据结构之链表
目录
一、链表是什么?
list -->ArrayList 顺序表:元素处在连续的内存空间上
list -->LinkList 链表:元素处在不连续的内存空间上
二、使用链表
1.定义链表
链表由两部分组成:
1.元素
2.节点
2.链表的作用:
解决顺序表中的"搬运"问题,中间插入/删除的时间复杂度是O(N).
- 顺序表插入元素 100 的过程
链表实现元素 100 的插入
删除同理,顺序表也是需要搬运的
链表删除元素 2
-
链表和顺序表的取下标操作
1.顺序表数组,索引(index)指向数组下标,顺序表是直接封装了数组,可以直接使 用index 来取元素。 2.链表不是数组,无法使用 index 来直接取下标,可以使用 get / set 方法取下 标,只不过此方法比顺序表的操作效率低。
3.单向链表和双向链表
- 单向链表:只能通过当前节点,找到下一个节点~,无法找到上一个节点
class Node {
int val; // 链表中要保存的元素
Node next; // 保存指向下一个结点的引用
}
- next == null 表示链表到达了末尾
- 双向节点:通过当前节点,能找到下一个节点,也可以找到上一个节点
class Node {
int val;
Node next;
Node prev;
}
4.带傀儡节点的链表和不带傀儡节点的链表
傀儡节点:不实际存储数据,占用位置
5.带环的链表和不带环的链表
-
不带环的链表:最后一个元素指向null
-
带环的链表:最后一个元素不指向空,而是指向链表的某个节点
总结:以上六种链表是正交的,可以随意任意组合,衍生出八种链表
链表的头结点:
针对单向链表,为了获取其头结点,通常使用头结点代替整个链表
6.链表遍历
package java;
//使用 Node 表示链表的节点
public class Node {
public int val;
public Node next = null;
public Node(int val){
this.val = val;
}
@Override
public String toString() {
return "[" + val + "]";
}
}
package java;
public class Main {
//此方法是创建出一个固定内容的链表
//使用头结点来代指整个链表
//方法返回头结点
public static Node createList(){
Node a = new Node(1);
Node b = new Node(2);
Node c = new Node(3);
Node d = new Node(4);
a.next = b;
b.next = c;
c.next = d;
d.next = null;
return a;
}
public static void main(String[] args) {
Node head = createList();
for (Node cur = head; cur != null; cur = cur.next ) {
System.out.println(cur.val);
}
}
}
输出结果
通过遍历,找到链表的最后一个结点
Node cur = head;
while (cur != null && cur.next != null){
}
System.out.println(cur.val);
运行结果
- 为了保持代码稳健,头结点不可以为空
- 如若不带傀儡节点的链表表示空,直接用 head = null 来表示;如若带傀儡节点的链表表示空链表,则使用 head.next = null 来表示。(带傀儡节点意味着无论如何都有一个节点)
通过遍历,找到链表的倒数第二个结点
int N = 3;
Node cur = head;
for (int i = 1; i < N; i++) {
cur = cur.next;
}
//此时 cur 指向的元素,就是正数第 N 个元素
System.out.println(cur.val);
运行结果:
通过遍历,找到链表的第 n 个结点。(从 1 开始,链表的长度 >= n)
int N = 3;
Node cur = head;
for (int i = 1; i < N; i++) {
cur = cur.next;
}
//此时 cur 指向的元素,就是正数第 N 个元素
System.out.println(cur.val);
运行结果:
通过遍历,计算链表中元素的个数
//每次循环访问一个节点之后 ,计数器count++
int count = 0;
for (Node cur = head;cur != null;cur = cur.next)
count++;
}
System.out.println(count);
运行结果:
通过遍历,找到链表的倒数第二个结点
通过遍历,找到链表中是否包含某个元素。
int toFind = 3;
Node cur = head;
for (; cur != null; cur = cur.next) {
if (cur.val == toFind) {
break;
}
}
if(cur != null){
System.out.println("找到了");
}
else{
System.out.println("没找到");
}
运行结果:
7.链表插入
中间插入
将100插入1和2之间
思路:
step1:先获取元素1 节点的引用,假设Node one 对应 1 节点的引用
step2:让新节点的 next 指向 2 节点(one.next)
newNode.next = one.next;
step3:让 one 的 next 指向新节点
one.next = newNode;
- 垃圾回收
public class Main {
public static Node creatList(){
//此处的 a b c d 四个引用为局部变量,方法使用完就会被销毁,在其后面new的对象在垃圾回收的时候销毁
//什么时候垃圾回收:没有任何(强)引用指向的时候
Node a = new Node(1);
Node b = new Node(2);
Node c = new Node(3);
Node d = new Node(4);
a.next = b;
b.next = c;
c.next = d;
d.next = null;
return a; //将a中存的地址返回到 Node head 中,即 Node head 指向 元素 1
}
public static void main(String[] args) {
Node head = creatList();
tips:对于带环链表是不是存在垃圾回收只剩环的情况呢?
答案是:不会的
理由:对于带环的链表,如果删除头结点指向,虽然其他节点也有引用吗,但是还是会被回收的,因为该节点对象为“不可达”,GC在分析对象能否被释放时,会对其进行可达性分析,即从一些特殊的引用开始进行搜索,例如:每个栈针中的局部变量,类的静态变量,对这些特殊的引用有一个别称-- gcroots。
链表元素的插入(不带傀儡节点)—头插入
- 头插入需要考虑的最大问题是将会影响头结点的引用。
第一步:让 newNode 的 next 指向链表的第一个节点
第二步:让head指向新节点
插入小结
对于无傀儡节点的中间插入和头插入是两种不同的思想和过程,对于开发者使用起来较为繁琐,故若引用傀儡节点,则可将头插入转换成中间插入,使得开发效率大大提高。
带傀儡节点的链表插入
- 中间插入
在节点 1 和 2 之间插入新节点 100
-
step0:准备工作
-
step1:
-
step2:
//先创建带傀儡节点的链表
Node head = creatListWithDummy();
Node newNode = new Node(100);
//在 1 和 2 之间插入元素,需要知道前一个(prev)元素的位置
//prev 是指向 1 的位置。prev 表示前一个元素
Node prev = head.next;
newNode.next = prev.next;
prev.next = newNode;
-
头插入
由于是带傀儡节点,对其头插入相当于插入到 head 的后面
Node prev = head;
newNode.next = prev.next;
prev.next = newNode;
- step0:准备工作
- step1:
- step2:
- tips:此处的头结点是傀儡节点,head 是一个班指向傀儡节点的引用;傀儡节点是无意义的,遍历的时候不遍历傀儡节点。
链表尾插(带头结点和不带头结点)
此处以不带头结点为例进行说明
- step:准备工作
- step1:
- step2:
空链表插入
对于空链表的插入,只需要让 head 的引用指向新的节点即可。因为空链表中无任何节点,故不存在“前一个位置”。
- 如若采用之前常规的方法插入是无法插入的
//空链表插入操作
public static void insertTail(Node head, int val) {
Node newNode = new Node(val);
if(head == null){
head = newNode;
return;
}
Node prev = head;
Node cur = prev;
while (cur.next != null){
cur = cur.next;
}//循环结束,cur 就是最后一个节点吧
newNode.next = prev.next;
prev.next = newNode;
}
public static void main(String[] args) {
//空链表插入操作
Node head = null;
insertTail(head,100);
print(head);
}
运行结果
《原因分析》
- tips1:此处主函数中的 head 和 插入函数(insertTail)中的 head 不一样。
- tips2:插入函数回调仅仅修改插入函数内部的 head 值,返回时,并不改变主函数中的head 值。
- 总结:故使用常规方法无法插将新元素入到空链表中。
正确做法如下:
- 给插入函数设置返回值
public static Node insertTail(Node head, int val) {
Node newNode = new Node(val);
if(head == null){
return newNode; //返回 newNode
}
Node prev = head;
Node cur = prev;
while (cur.next != null){
cur = cur.next;
}//循环结束,cur 就是最后一个节点吧
newNode.next = prev.next;
prev.next = newNode;
return head; //返回 head
}
public static void main(String[] args) {
//空链表插入操作
Node head = null;
head = insertTail(head,100); //接收返回值
print(head);
}
运行结果:
8.链表删除(不带傀儡节点)
1.删除一般节点
- step1
- step2
时间复杂度为O(N)–按值删除
//(1)删除节点(按照值删除)
public static void remove(Node head,int value){
//1.先找到 value 值对应的位置
// 还需要找到 val 的前一个位置
Node prev = head;
//遍历循环找到 value 的前一个位置
while (prev != null
&& prev.next != null
&& prev.next.val != value){
prev = prev.next;
}
//循环结束之后,prev 指向了待删除节点的前一个节点
if (prev == null || prev.next == null){
return; //没有找到值为 val 的节点
}
//2.删除操作,toDelete 指向要被删除的节点
Node toDelete = prev.next;
prev.next = toDelete.next;
}
时间复杂度为O(N)–按位置删除
//(2)删除节点,按照位置来删除
public static void remove(Node head,Node toDelete){
//step1:找到 toDelete 的前一个节点的位置
Node prev = head;
while (prev != null && prev.next != toDelete){
prev = prev.next;
}
if(prev == null){
return; //没找到
}
prev.next = toDelete.next;
}
时间复杂度为O(1)的按位置删除操作:
//时间复杂度为O(1)的按位置删除操作
public static void remove2(Node head,Node toDelete){
Node nextNode = toDelete.next;
toDelete.val = nextNode.val;
toDelete.next = nextNode.next;
}
- tips: 该方法无法处理最后一个节点
2.一般节点按照下标删除
//按照下标(抽象概念)删除
public static void remove3(Node head,int index){
if (index < 0 || index >= size(head)){
return;
}
//如果 index = 0,则删除头结点(后面讲)
if(index == 0){
//TODO
}
//step1:找到待删除节点前一个位置的节点,即:index - 1
Node prev = head;
for (int i = 0;i < index -1;i++){
prev = prev.next;
}
//循环结束之后,prev 指向了待删除节点的前一个位置
//step2:进行删除
Node toDelete = prev.next;
prev.next = toDelete.next;
}
链表删除总结
- 链表的删除大多数情况下都需要遍历找到前一个元素的位置,故其时间复杂度是O(N)
- 链表和顺序表的时间复杂度无法具体比较出孰优孰劣。其两者的本质区别是顺序表的存储再内存上是连续的空间;而链表的存储在内存上可以不连续。
链表删除头结点
链表删除总结
为了实现形参的顺利返回,对原先实现的删除函数需要设置返回值 Node。
//(1)删除节点(按照值删除) 时间复杂度为O(N)
public static Node remove(Node head, int value){
if(head == null){
return null;
}
//删除头结点
if (head.val == value){
//删除的节点即为头结点
head = head.next;
return head;
}
//1.先找到 value 值对应的位置
// 还需要找到 val 的前一个位置
Node prev = head;
//遍历循环找到 value 的前一个位置
while (prev != null
&& prev.next != null
&& prev.next.val != value){
prev = prev.next;
}
//循环结束之后,prev 指向了待删除节点的前一个节点
if (prev == null || prev.next == null){
return null; //没有找到值为 val 的节点
}
//2.删除操作,toDelete 指向要被删除的节点
Node toDelete = prev.next;
prev.next = toDelete.next;
return head;
}
//(2)删除节点,按照位置来删除 时间复杂度为O(N)
public static Node remove(Node head,Node toDelete){
if (head == null){
return null;
}
if(head == toDelete){
//要删除的即为头结点
head = head.next;
return head;
}
//step1:找到 toDelete 的前一个节点的位置
Node prev = head;
while (prev != null && prev.next != toDelete){
prev = prev.next;
}
if(prev == null){
return null; //没找到
}
prev.next = toDelete.next;
return head;
}
//时间复杂度为O(1)的按位置删除操作
public static Node remove2(Node head,Node toDelete){
if(head == null){
return null;
}
if (head == toDelete){
head = head.next;
return head;
}
Node nextNode = toDelete.next;
toDelete.val = nextNode.val;
toDelete.next = nextNode.next;
return head;
}
//注意:该方法无法处理最后一个节点
public static int size(Node head){
int size = 0;
for (Node cur = head;cur != null;cur = cur.next){
size++;
}
return size;
}
//按照下标(抽象概念)删除
public static Node remove3(Node head,int index){
if (index < 0 || index >= size(head)){
return head;
}
//如果 index = 0,则删除头结点
if(index == 0){
head = head.next;
}
//step1:找到待删除节点前一个位置的节点,即:index - 1
Node prev = head;
for (int i = 0;i < index -1;i++){
prev = prev.next;
}
//循环结束之后,prev 指向了待删除节点的前一个位置
//step2:进行删除
Node toDelete = prev.next;
prev.next = toDelete.next;
return head;
}
主函数调用
public static void main(String[] args) {
Node head = creatList();
head = remove(head,1);
print(head);
}
}
运行结果:
9.链表删除(带傀儡节点)
1.删除原理
-
step1
-
step2
2.删除过程
//带傀儡节点的删除
public static void removeWithDummy(Node head,int val){
//使用带傀儡节点的好处:
// 1.避免了 head 引用修改的问题
// 2.避免了删除头结点需要判断的问题
//循环寻找 val 匹配的值
Node prev = head;
while (prev != null && prev.next != null
&& prev.next.val != val){
prev = prev.next;
//循环结束的两种情况:
//1.prev 到达了链表末尾也没找到和 val 匹配的值
//2.找到了和 prev 匹配的值
if (prev == null && prev.next == null){
//没找到对应节点
return;
}
//找到了对应节点
Node toDelete = prev.next;
prev.next = toDelete.next;
return;
}
}