数据结构与算法--链表

概述

数组需要连续的内存空间来储存,所以对内存要求比较高,如果我们申请一个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;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值