概述
数组需要连续的内存空间来储存,所以对内存要求比较高,如果我们申请一个100MB的数组,如果内存中没有连续的,内存足够大的空间时,即使剩余总内存大于100MB,也会申请失败
链表需要不连续的内存空间来储存,他通过指针,把一组不连续的内存块连接起来使用,所以如果申请100MB的链表,不会有问题
常见的链表有,单链表,循环链表,和双向链表,我们先看一下单链表
单链表
链表通过指针,将一组不连续的内存串联在一起,其中我们把内存块称作链表的结点,为了把内存块串联起来,结点中除了储存数据还要储存下一个结点的地址,我么叫这个为后继指针 next
其中有俩个结点比较特殊
头结点:我们把第一个结点叫做头结点,用来记录链表的基地址,我们可以用它来遍历整条链表
尾节点:我们把链表的最后一个结点,称作尾节点,他的next为null
链表的插入和删除
由于数组是连续的内存空间,所以插入和删除的时候,会做大量的数据迁移,时间复杂度为O(n),而在链表中插入和删除一个数据,我们并不需要保持内存的连续性迁移结点,因为链表的内存本来就是不连续的,所以在链表中插入和删除只需要改变前一个结点的next,时间复杂度为O(1)
链表的查找
数组支持随机访问,所以查找的时间复杂度为O(1),但是链表是内存不连续的,不支持随机访问,每次访问都需要从头结点遍历,时间复杂度为O(n)
循环链表
循环链表和单链表唯一的差别在尾节点,单链表的尾节点next指向null,而循环链表的next指向头结点,他像一个环一样,首尾相连
循环链表的优点是,尾节点到头结点比较方便,当要处理的数据,有环形结构时,就适合采用循环链表
双向链表
双向链表顾名思义:他有俩个方向,结点除了储存后续指针next下一个结点的地址外,还储存了前驱指针prev 上一个结点的地址
双向链表需要存储俩个指针,同样的数据需要的空间比单链表更大,但是双向链表更加方便,可以双向遍历
双向链表的优点
1 插入删除更加高效
比如删除你需要下面几步
1 在链表中找到删除的节点,时间复杂度O(n)
2 查找到删除节点的前驱节点,时间复杂度O(n)
3 删除该节点,时间复杂度O(1)
因为想要删除节点必须要,找到该节点的前驱节点,改变其next,才可以实现,所以需要第二步
单链表和双向链表1和3步都一样,但是第2步,单链表需要O(n),但是双向链表存储了前驱指针,所以只需要O(1),所以双向链表更加高效
2 对于有序链表查找更加高效
我们可以记录上次查询的节点p,下次在进行查询,需要跟p比较然后,决定向后遍历,还是向前遍历
这里有个知识点,空间换时间,当内存比较充足时,我们需求代码执行更快,我们可以选择空间复杂度较高,时间复杂度较低的算法或数据结构,相反的反之。
链表和数组大比拼
如何写出正确的链表代码
留意边界条件的处理
在写任何代码时,不要以实现业务正常为目的,一定要多想想,你的代码在运行的时候会遇到什么边界情况导致的异常,遇到这样的情况该如何应对,这样写出来的代码才会更急健壮。链表也不例外
我们经常用于检查链表的边界条件
1 链表为空时,是否可以正常工作
2 链表只有一个结点时,是否可以正常工作
3 链表只包含俩个结点时,是否可以正常工作
4 代码逻辑在处理头结点和尾节点时,是否可以正常工作
一般来说,满足了这四个边界条件,链表基本就可以用了
练习
自定义一个单链表
public class MyLinkedList {
//存储首链
private Box firstBox = null;
//记录元素数量
private int count = 0;
//模拟add()方法
public void add(Object obj){
//1.实例化一个Box
Box box = new Box();
box.obj = obj;
//计数器
count++;
//2.判断是否是首链
if(this.firstBox == null){
this.firstBox = box;
}else{
//3.遍历,找到最后一个链
Box tempBox = this.firstBox;
while(tempBox.next != null){
tempBox = tempBox.next;
}
//4.此时tempBox就是最后一环,将box连接上去
tempBox.next = box;
}
}
//模拟size()方法
public int size(){
return this.count;
}
//模拟get()方法
public Object get(int index){
if(index >= count){
return null;
}
//从首链开始找,并且开始计数
int n = 0;
Box tempBox = this.firstBox;
while(tempBox.next != null && n != index){
tempBox = tempBox.next;
n++;
}
return tempBox.obj;
}
private class Box{
public Object obj;
public Box next;
}
}
单链表反转
(1)迭代法。先将下一节点纪录下来,然后让当前节点指向上一节点,再将当前节点纪录下来,再让下一节点变为当前节点
public Node reverse(Node node) {
Node prev = null;
Node now = node;
while (now != null) {
Node next = now.next;
now.next = prev;
prev = now;
now = next;
}
return prev;
}
(2)递归方法。先找到最后一个节点,然后从最后一个开始反转,然后当前节点反转时其后面的节点已经进行反转了,不需要管。最后返回原来的最后一个节点
public Node reverse2(Node node, Node prev) {
if (node.next == null) {
node.next = prev;
return node;
} else {
Node re = reverse2(node.next, node);
node.next = prev;
return re;
}
}
链表中环的检测
1 快慢指针法
俩个指针P1,P2同时从头遍历链表
P1是慢指针一次遍历一个结点
P2是快指针一次遍历俩个结点
如果没有环,P1和P2会先后遍历完链表
如果有环,P1和P2会先后进入环进行循环,并在某一次相遇,所以我们只要判断P1和P2相遇了,就可以判断链表中是否有环
public class LinkADT<T> {
/**
* 判断是否有环 快慢指针法
*
* @param node
* @return
*/
public static boolean hasLoopV1(SingleNode headNode) {
if(headNode == null) {
return false;
}
SingleNode p = headNode;
SingleNode q = headNode.next;
// 快指针未能遍历完所有节点
while (q != null && q.next != null) {
p = p.next; // 遍历一个节点
q = q.next.next; // 遍历两个个节点
// 已到链表末尾
if (q == null) {
return false;
} else if (p == q) {
// 快慢指针相遇,存在环
return true;
}
}
return false;
}
}
将俩个有序链表合并
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
ListNode head = null;
if (l1.val <= l2.val){
head = l1;
head.next = mergeTwoLists(l1.next, l2);
} else {
head = l2;
head.next = mergeTwoLists(l1, l2.next);
}
return head;
删除链表倒数第n个结点
注意事项
链表中的节点个数大于等于n
先让一个指针走找到第N个节点,然后再让一个指针指向头结点,然后两具指针一起走,直到前一个指针直到了末尾,后一个指针就是倒数第N+1个结点,删除倒数第N个结点就可以了。
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pa = head;
ListNode pb = head;
// 找到第n个结点
for (int i = 0; i < n && pa != null; i++) {
pa = pa.next;
}
if (pa == null) {
head = head.next;
return head;
}
// pb与pa相差n-1个结点
// 当pa.next为null,pb在倒数第n+1个位置
while (pa.next != null) {
pa = pa.next;
pb = pb.next;
}
pb.next = pb.next.next;
return head;
}
思路很简单,只有两种出现的情况,1、链表的长度刚刚好等于n,也就是说删除表头节点,2、链表长度大于n,那么我们先定义两个表头,一个后移n位,然后两个链表同时后移
求链表的中间点
利用快慢指针:
设置两个指针slow和fast,两个指针同时向前走,fast指针每次走两步,slow指针每次走一步,直到fast指针走到最后一个结点时,此时slow指针所指的结点就是中间结点
public Node method(Node head) {
Node p=head;
Node q=head;
while(q!=null&&q.next!=null&&q.next.next!=null) {
p=p.next;
q=q.next.next;
}
return p;