算法 - 链表

单链表反转

将链表的头结点变成尾结点

/**
     * 翻转单链表
     *
     * @param head
     * @return
     */
    public static Node reverseLinkedList(Node head) {
        // 预存前节点
        Node pre = null;
        // 预存后节点
        Node next = null;

        while (head != null) {
            // 取出当前节点的后一个节点,保存下来
            next = head.getNext();
            // 翻转,将当前节点的next指向保存下来的前节点
            head.setNext(pre);

            // 将当前节点作为下一个节点的前节点,预保存下来
            pre = head;

            // 将当前接节点的指针向后移动,开启下一轮节点的翻转
            head = next;
        }

        return pre;
    }

双向链表反转

public static DoubleNode reverseDoubleList(DoubleNode head) {
        // 预存前节点
        DoubleNode pre = null;
        // 预存后节点
        DoubleNode next = null;

        while (head != null) {
            // 取出当前节点的后一个节点,保存下来
            next = head.getNext();
            // 翻转,将当前节点的next指向保存下来的前节点
            head.setNext(pre);
            //  翻转,将当前节点的pre指向保存下来的后节点
            head.setPre(next);
            // 将当前节点作为下一个节点的前节点,预保存下来
            pre = head;
            // 将当前接节点的指针向后移动,开启下一轮节点的翻转
            head = next;
        }

        return pre;
    }

单链表删除某一个值的节点

假设我们现有单链表 1 ->2->3->4->6->7…->null。如果我们需要删除value为某个值的节点,如需要删除value=1的节点,或者删除value=7的节点,我们该怎么做呢?

public static Node removeValueList(Node head, int num) {

        // 如果链表的头结点以及后续节点,都是需要删除的节点,则head需要向后移动
        while (head != null) {
            if (head.getValue() != num) {
                break;
            }

            head = head.getNext();
        }

        // 此时head来到了第一个不需要删除的位置
        
        // 将当前节点的前节点保存下来,如果是head则为自身
        Node pre = head;
        // 从head开始遍历后续每一个节点,cur为当前被遍历的节点
        Node cur = head;

        while (cur != null) {
            if (cur.getValue() == num) {
                // 当前节点需要被删除,将当前节点的子节点,挂载到当前节点的父节点的next上,用于替代当前节点
                pre.setNext(cur.getNext());
            } else {
                // 当前节点不需要被删除,前节点指针向后移动
                pre = cur;
            }

            // 当前节点指针向后移动
            cur = cur.getNext();
        }

        return head;
    }

单链表,链表长度为基数则返回中点元素,偶数则返回上中点元素

这道题什么意思呢?
如现有链表 1 -> 2 -> 3 -> 4 -> 5,则需要返回元素3
如链表1 -> 2 -> 3 -> 4, 则中间元素为2,3,上中点元素为2。

分析:
目前我们只有链表的头结点head,根据需求,当我们链表遍历到中点/上中点位置时,我们必须要知道我们已经到达了中点位置。

我们可以使用快慢指针来完成这个功能,但是具体的边界值该如何确立呢?
慢指针移动一位,快指针需要移动两位。那为什么设置为移动两位呢?
我们模拟选快慢指针的移动过程:
0->1 代表从index为0的位置,移动到index=1的位置

慢指针0 -> 1,快指针0 ->2 , 快慢指针步长差值为2-1 =1 =慢指针的index
慢指针1 -> 2,快指针2->4, 快慢指针步长差值为4-2 =2 =慢指针的index
慢指针2 -> 3,快指针4->6, 快慢指针步长差值为6-3 =3 =慢指针的index
慢指针3 -> 4,快指针6->8, 快慢指针步长差值为8-4 =4 =慢指针的index
慢指针4 -> 5,快指针8->10,快慢指针步长差值为10-5 =5 =慢指针的index

假设我们现在慢指针正好在中点元素上,则链表的总长度为慢指针的index*2,换句话来讲,从链表的尾结点到我们的慢指针(此时为中间节点),这中间的长度正好等于慢指针的index。

我们通过设置指针的移动速度,正好可以让快慢指针之间正好间隔出半个链表的长度。当我们的慢指针到达中点元素时,我们快指针已经到达链表的末尾或者接近到达末尾。

由于题目中要求,如果链表长度是偶数,则返回的是上中点,也就是说在长度为基数情况下,快慢指针同步移动,如果是偶数的情况下,慢指针其实不动。这就要求快慢指针的起始位置并不是在一起的,而是一前一后,中间需要一个位差来调节技术偶数的情况。

当起始位置相差1之后,这两种情况就可以归并为一种场景考虑了。

如果链表长度为0,1,没有中点元素可以返回,如果长度为2,head即为上中点元素。这三种情况就不再考虑了。

我们假设现在链表只有3个元素的情况:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
所以当慢指针在中点位置,或者上中点位置时,fast指针有两种可能,一种在tail位置,此时fast.next=null;还有一种可能是在tail的前一个节点位置,此时fast.next.next = null

public static Node midOrUpMidNode(Node head) {

        // node -> node -> node(null)
        // 如果链表长度为0,或者1,或者2,当前head即可满足题意
        if (head == null || head.getNext() == null || head.getNext().getNext() == null) {
            return head;
        }
        // 此时链表长度必然大于等于3
        // 设置快慢指针,指定步长为1
        Node slow = head.getNext();
        Node fast = head.getNext().getNext();

        while (fast.getNext() != null && fast.getNext().getNext() != null) {
        	// 此时slow还没有到达  中点/上中点  位置,可以继续向后移动
            slow = slow.getNext();
            fast = fast.getNext().getNext();
        }

        return slow;
    }

单链表,链表长度为基数则返回中点元素,偶数则返回下中点元素

通过前面的分析,我们可以简单的分析出,slow和fast是需要同步移动的,中间不需要一个位差来调节奇数偶数的情况

public static Node midOrDownMidNode(Node head) {

        // node -> node -> node(null)
        // 如果链表长度为0,或者1,或者2,当前head即可满足题意
        if (head == null || head.getNext() == null || head.getNext().getNext() == null) {
            return head;
        }
        Node slow = head.getNext();
        Node fast = head.getNext();

        while (fast.getNext() != null && fast.getNext().getNext() != null) {
            slow = slow.getNext();
            fast = fast.getNext().getNext();
        }

        return slow;
    }

单链表,链表是否是回文结构

什么是回文?像1221,12321这种左右对称的便是回文结构,那对应到链表又是什么情况呢?
在这里插入图片描述
由于单项链表只能从head遍历到tail,而判断是否回文,我们需要使用head元素与tail元素进行比较,两者的顺序正好相反。

在这里插入图片描述
我们可以借助栈先进后出的特性,正向遍历一次链表,然后将元素压入栈中。这样链表的head节点第一个入栈,成为栈里最里面一个元素,而链表的tail最后一个入栈,成为栈的栈顶元素。当进行出栈pop操作时,先出去的实际是tail节点数据,最后才是head节点数据。借助stack,我们可以实现类似从链表尾部向前遍历的功能。

  public static boolean isPalindrome(Node head) {
        Stack<Node> stack = new Stack<>();

        Node cur = head;

        // 将链表中所有元素入栈
        while (cur != null) {
            stack.push(cur);
            cur = cur.getNext();
        }

        // 每一次正向遍历链表,从栈中pop出尾结点数据,如果数据不一致则不是回文
        while (head != null) {
            if (head.getValue() != stack.pop().getValue()) {
                return false;
            }

            head = head.getNext();
        }

        return true;
    }

是否还有另外的解法,不借助于stack呢?
我们来分析下这个功能,当我们遍历到中点元素的时候,此时如果我们能够从tail向head,也遍历到中点位置,那我们可以实现同样的功能。如何识别我们到了中点位置?不就是我们上面讲过的那个方法嘛。
在这里插入图片描述
当我们遍历到中点元素的时候,需要将后续的元素进行翻转,此时原链表会被分割成两部分:
在这里插入图片描述

public static boolean isPalindrome2(Node head) {
        // 找到中点元素
        Node midNode = midOrUpMidNode(head);

        // 从中点元素的next开始,将后续链表进行翻转
        Node reverseHead = reverseLinkedList(midNode.getNext());

        // 现在已经将原有链表切割成了两个链表了,将两个链表依次进行比较
        Node originCurrent = head;
        Node reverseCurrent = reverseHead;

        boolean isPalindrome = true;
        while (originCurrent != null && reverseCurrent != null) {
        	// 判断正向和逆向的元素是不是一样的
            if (originCurrent.getValue() != reverseCurrent.getValue()) {
                isPalindrome = false;
                break;
            }

            originCurrent = originCurrent.getNext();
            reverseCurrent = reverseCurrent.getNext();
        }

        // 将两个链表重新恢复到原始链表
        midNode.setNext(reverseLinkedList(reverseHead));
        return isPalindrome;
    }

这个可能不是很好理解,我们结合实际运行结果来看下:
在这里插入图片描述
我们原始链表中有9个元素,id分别为1,2,3,4,5,6,7,8,9,value为1,2,3,4,5,4,3,2,1,原始链表如下:
在这里插入图片描述
当我们找到中点元素后,对后续的链表进行翻转,原始链表就被切割成了2份:
在这里插入图片描述
此时示意图如下:
在这里插入图片描述
此时我们要做的就是从正向的head向右遍历链表,逆向的head向左遍历链表,查看移动相同位数的情况下,数值是否相等。

最后再将拆分后的两个链表还原成原始的链表。

单链表,如何按照某一个值进行分区

如现有链表 1->4->5->3->6->3,以3位界限,区分 小于3 的区域 ,等于3区域,大于3的区域。

在这里插入图片描述
按照需求,需要将上面的原始链表最终转换为下面啊的链表。

方案一:
在这里插入图片描述
可以借助三个外部数组,先遍历一次链表,分别将数据放入小于3的数组里,等于3 的数组里,大于3的数组里。最后分别遍历3个数组,组装出新的链表返回即可

方案二:
**加粗样式**
我们首先定义出6个引用变量,类型为链表的Node类型,分别代表 小于区的head和tail,等于区的head和tail,大于区的head和tail。
然后让指针current从head开始进行遍历:

  1. current=1时,1属于小于区,将small head和small tail都指向1,current指针移向next,同时1断开next:
    在这里插入图片描述

  2. current到达4,归属于大于区,步骤同上:
    在这里插入图片描述

  3. current到达5,归属于大于区域,由于big head 和big tail都是引用类型,通过这两个引用,我们就可以找到节点4,现在我们需要节点4的next指向5,同时设置5为big tail:
    在这里插入图片描述

  4. current=3,归属等于区,同步骤 1 / 2:
    在这里插入图片描述

  5. current=6,归属于大于区,步骤同3:
    在这里插入图片描述

  6. current=3,归属于等于区,方法同上:
    在这里插入图片描述

  7. 根据引用关系,将small tail 的next设置为 equal head ,将equal tail 的next设置为 big head,这样便构建出一个新的链表:
    在这里插入图片描述
    这样我们就可以得到新的链表 1->3->3->4->5->6

单链表,如何判断其中有环

我们来分析下单链表有环的情况下有几种可能性:
在这里插入图片描述

  1. head和tail直接形成环
  2. 中间某一个环节形成环

那我们如何判断一个链表中是否有环呢?

方案一:
借助hashSet,我们知道HashSet中数据是不能重复的,如果我们将链表中的每一个元素都放入HashSet中,一旦出现add失败,则链表中一定存在环结构,且add失败的节点即为环交接的节点。

方案二:
在这里插入图片描述
可以使用快慢指针,慢指针每次走一步,快指针每次走两步,如果链表存在环,则快慢指针一定会在换上的某一个节点相遇。

为演示,随机取一个点做为相遇点:
在这里插入图片描述
那我们如何确定环的交接点是哪个呢?从slow和fast指针的相遇处开始,fast指针回到head节点,slow还停留在原地,如何两个指针都以步长1开始移动,最终两个指针的相遇点即为环的交汇点:
在这里插入图片描述

/**
     * 找到链表的第一个入环节点,如果链表无环则返回null
     *
     * @param head
     * @return
     */
    public static Node getLoopNode(Node head) {
        if (head == null || head.getNext() == null || head.getNext().getNext() == null) {
            return null;
        }

        Node slow = head.getNext();
        Node fast = head.getNext().getNext();

        // 找到快慢指针相遇的点
        while (slow != fast) {
            if (fast.getNext() == null || fast.getNext().getNext() == null) {
                return null;
            }

            fast = fast.getNext().getNext();
            slow = slow.getNext();
        }

        // fast回到head,开始依次往下走,寻找环的交接点
        fast = head;
        while (slow != fast) {
            fast = fast.getNext();
            slow = slow.getNext();
        }

        return slow;
    }

判断两个单链表是否相交

链表无环

两个链表不想交:在这里插入图片描述
相交的情况:
在这里插入图片描述
两个链表一旦相交,从相交节点开始,必然只会有一条线路,因为节点的next指针只有一个,所以肯定不会有分叉的情况出现。

所以我们怎么判断两个链表会相交呢?

方案一:
和判断是否有环一样,两个链表相交,那必然是会公用同一个节点的。我们还是可以借助hashSet:
遍历链表1,将链表1中的所有node节点放入hashSet中
再遍历链表2,将链表2的node节点也放入hashSet中。如果出现add失败,则add失败的节点,即为交汇点。

方案二:
在这里插入图片描述
根据我们前面的分析,如果两个链表有相交,则其尾结点一定会重合的。如果交汇点就是尾结点,则尾结点重合。如果交汇点事尾结点的前一个节点,则尾结点还是会重合的。

按照这个结论,我们往前进行推理:
假设现在两个链表,一个长一个短。长度分别为5和7。则从尾结点开始往前的5个长度之内,两个链表必有交汇点。换而言之,短链表从head开始遍历,长链表从 第(7-5)+1个节点开始,和以相同的步长开始遍历,在5个长度之内,必然会找到交汇点。

 /**
     * 如果两个无环链表相交,返回第一个交汇节点
     *
     * @param head1
     * @param head2
     * @return
     */
    public static Node noLoop(Node head1, Node head2) {
        if (head1 == null || head2 == null) {
            return null;
        }

        Node current1 = head1;
        Node current2 = head2;

        // 记录两个链表的长度之差
        int lengthDiff = 0;

        // 遍历第一个链表,获取其长度
        while (current1.getNext() != null) {
            lengthDiff++;
            current1 = current1.getNext();
        }

        // 遍历第二个链表,获取其与第一个链表的长度的差值
        while (current2.getNext() != null) {
            lengthDiff--;
            current2 = current2.getNext();
        }

        // 此时两个链表都到达了尾结点,如果两个尾结点还是没有交汇,说明两个链表不会相交
        if (current1 != current2) {
            return null;
        }

        // 强制current1指向长度较长的那个链表
        current1 = lengthDiff > 0 ? head1 : head2;
        // 强制current2指向长度较短的那个链表
        current2 = current1 == head1 ? head2 : head1;

        // 挪动长链表的指针,直至长链表剩余长度与短链表长度相同
        lengthDiff = Math.abs(lengthDiff);
        while (lengthDiff != 0) {
            lengthDiff--;
            current1 = current1.getNext();
        }

        // 长度相同之后,两个链表同步向后移动,两者最终会同步到达交汇点,也即current1 == current2
        while (current1 != current2) {
            current1 = current1.getNext();
            current2 = current2.getNext();
        }

        return current1;
    }

链表有环

在这里插入图片描述
总结下来会有这三种场景。
我们可以通过上面的getLoopNode(head),找到每一个链表的入环节点,假设链表1的入环节点为loop1,链表2的入环节点是loop2。

分析一下情况1和情况2,我们可以看到两个链表的入环节点都是A,也就是链表1的loop1=A,链表2的loop2=A,此时loop1=loop2。此时的节点A必然是两个链表的重合点。我们可以不再关系节点A后续的所有节点了,只需要关心链表1从head到节点A,链表2从head到节点A,这这区间链表上,哪一个节点是交汇点。这种场景下,可以使用上面”无环链表的交汇点”的方案进行解决。这种场景我们只要判断loop1 == loop2:

在这里插入图片描述
我们要如何判断是否是场景3呢?
在这里插入图片描述
链表1的入环节点是A,而链表2的入环节点是B。这两个节点是不可能相等的。由于这两个点都位于环上。所以从A节点出发,通过next必然可以到达B节点,同理,B节点通过next必然也可以到达A节点。所以这种情况下,只要从loop1可以遍历到loop2,或者从loop2可以遍历到loop1,那么loop1和loop2都是交汇点。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值