单链表的应用之链表反转、快慢指针

1. 单链表实现代码

定义简单的单链表实现代码,里面定义单链表每个节点的组成形式,是一个内部类,实现了最基本的增、删、查、迭代器遍历功能。

package com.calarqiang.linear;
import java.util.Iterator;

/**
 * @author calarqiang
 * @create 2022-07-23-13:54
 * 实现单向链表
 */
public class LinkList<T> implements Iterable<T> {
    private int N;
    private Node<T> head;

    //    内部节点类
    private class Node<T> {
        private T elem;
        private Node<T> next;

        public Node(T elem, Node<T> next) {
            this.elem = elem;
            this.next = next;
        }
    }

    public LinkList() {
        this.N = 0;
//        初始化头结点
        this.head = new Node(null, null);
    }

    //    获取元素个数
    public int getLen() {
        return this.N;
    }

    //    清空链表
    public void clear() {
        this.head = null;
        this.N = 0;
    }

    //    在末尾插入一个元素
    public void insert(T elem) {
        Node newNode = new Node(elem, null);
//        寻找到最后一个节点
        Node<T> p = head;
        while (p.next != null) {
            p = p.next;
        }
        p.next = newNode;
        this.N++;
    }

    //    在链表指定索引处插入一个元素
    public void insert(int index, T elem) {
//        判断插入位置是否合法
        if (index < 0 || index > this.N) {
            System.out.println("插入位置不合法!");
            return;
        }
        Node newNode = new Node(elem, null);
//        找到该索引的上一个索引处元素
        Node<T> p = head;
        for (int i = 0; i <= index - 1; i++) {
            p = p.next;
        }
        newNode.next = p.next;
        p.next = newNode;
        this.N++;
    }

    //    判断链表是否为空
    public boolean isEmpty() {
        return this.N == 0;
    }

    //    删除指定位置的元素
    public T remove(int index) {
//        判断位置是否合法
        if (index < 0 || index >= this.N) {
            System.out.println("删除位置不合法!");
            return null;
        }
        Node<T> p = head;
//        找到指定元素的上一个元素
        for (int i = 0; i <= index - 1; i++) {
            p = p.next;
        }
//        用一个元素记录要删除的节点的值
        T elem = p.next.elem;
        p.next = p.next.next;
        return elem;
    }

    //    寻找某个元素所在的索引处
    public int getElem(T elem) {
        Node<T> p = head;
        for (int i = 0; p.next != null; i++) {
            p = p.next;
            if (p.elem.equals(elem)) {
                return i;
            }
        }
//        没有找到,返回-1
        return -1;
    }

// 通过重写Iterable接口中的iterator方法来实现迭代器
    @Override
    public Iterator<T> iterator() {
        return new MyIter();
    }
//  编写一个内部类,来实现Iterator接口,并重写hasNext和next方法。    
    private class MyIter implements Iterator<T> {
        private Node<T> cursor;

        public MyIter() {
            this.cursor = head;
        }

        @Override
        public boolean hasNext() {
            return this.cursor.next != null;
        }

        @Override
        public T next() {
            cursor = cursor.next;
            return cursor.elem;
        }
    }
}

2. 单链表反转

原理:所谓链表反转,就是让原来的单链表指向发生变化,从左往右---->从右往左,指向发生反转。
单链表反转原理图

递归调用反转节点方法原理图

定义两个重载方法,第一个方法对整个链表进行反转,第二个重载方法为反转单个节点的方法。

 //    反转整个单链表
    public void reverse() {
//        判断单链表是否为空,如果为空,就不需要反转
        if (head.next == null) {
            return;
        }
//        调用反转节点方法,对每个节点进行反转
        reverse(head.next);
    }

    //    反转每个结点
    public Node<T> reverse(Node curr) {
//        递归退出条件
        if (curr.next == null) {
            head.next = curr;
            curr.next = null;
            return curr;
        }
//    如果当前节点的下一个节点不为null,则反转下一个节点,这里返回的是要反转的节点的下一个节点,此时为当前反转的上一个节点了
        Node prev = reverse(curr.next);
//        让原来的下一个节点变成当前节点的上一个节点,并让当前节点的下一个节点为null,返回当前节点
        prev.next = curr;
        curr.next = null;
        return curr;
    }

3. 快慢指针

字面意思,就是一个指针移动的快,一个指针移动的慢,通过这个两个指针移动的特点,就可以完成相应的需求。

可以通过快慢指针,完成下面几个需求。

3.1 寻找中间值

实现原理:初始时,快指针和慢指针均指向链表的头结点,快指针每次往后移动两个节点,慢指针每次往后移动一个位置,这样,快指针到达链表的尾部时,慢指针刚好到达链表的中点位置。

  • 元素个数为奇数时,快指针可以走到奇数元素+1的位置,比如长度n=5时,快指针可以走到6的位置,刚好走6/2=3步,慢指针也就走到了第三个节点,正好是中间位置。
  • 元素个数为偶数时,快指针可以走到偶数元素个数位置,比如长度n=4,快指针可以走到4的位置,刚好走4/2 = 2步,慢指针也就走到了第二个节点,也是中间位置。

实现代码

public T findCenter(){
// 初始化快慢指针
        Node<T> fast = head,slow = head;
// 快慢指针移动条件        
        while(fast!=null && fast.next!=null){
            fast = fast.next.next;
            slow = slow.next;
        }
// 返回慢指针指向的值(中间值)
        return slow.elem;
    }
3.2 判断单链表是否有环

后面的测试都是基于环来判断,前面单链表的创建直接使用尾插法,因此这里重新定义一个单链表结构,方便下面应用的测试。

public class LinkListCircle<T> {
    private Node<T> head;

    public LinkListCircle() {
        this.head = new Node<>(null, null);
    }

    private class Node<T> {
        private T elem;
        private Node<T> next;

        public Node(T elem, Node<T> next) {
            this.elem = elem;
            this.next = next;
        }

        public void setNext(Node<T> node) {
            this.next = node;
        }

    }

    public void createCircle() {
        Node<Integer> a = new Node<>(1, null);
        Node<Integer> b = new Node<>(2, null);
        Node<Integer> c = new Node<>(3, null);
        Node<Integer> d = new Node<>(4, null);
        Node<Integer> e = new Node<>(5, null);
        Node<Integer> f = new Node<>(6, null);
        a.next = b;
        b.next = c;
        c.next = d;
        d.next = e;
        e.next = f;
        f.next = b;
        head.setNext((Node<T>) a);
    }
    }

有时候单链表会存在环的情况,此时会有两种类型的环,一种类型是“6”字型,另一种是“O”字型号(循环单链表)。但是实现的逻辑都是采用快慢指针来实现,因为快指针比慢指针每次多走一个结点,因此总有一个时刻,它们会相遇【证明参考:证明,如果相遇,则证明有环,反之,如果快指针指向空了,那么就证明没有环存在。
简单证明:参考这里

“6字型”
在这里插入图片描述

第一次移动
在这里插入图片描述

第二次移动
在这里插入图片描述

第三次移动 【重合】
在这里插入图片描述

“O字型”
在这里插入图片描述

第一次移动
在这里插入图片描述

第二次移动
在这里插入图片描述

第三次移动
在这里插入图片描述

第四次移动【重合】
在这里插入图片描述
实现代码:

 public boolean isCircle(){
 //定义快慢指针
        Node<T> fast=head,slow = head;
 // 循环条件,快指针指向元素或下一个元素不能为null
        while(fast!=null && fast.next!=null){
            fast = fast.next.next;
            slow = slow.next;
 // 判断快慢指针是否相遇
            if(Objects.equals(fast,slow)){
                return true;
            }
        }
        return false;
    }
3.3 有环单链表的入口

如果单链表存在环,那么环的入口在哪里?怎样找到它?也是采用快慢指针的方式,当快指针和慢指针第一次重合之后,重新让一个指针指向入口处,然后让原来慢指针与其同步移动,当二者相遇时,即为入口处。具体证明参见这里。单链表入口原理解释
代码实现

public T findEntrance() {
        Node<T> fast = head, slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
//            判断二者是否相遇,相遇,有环,可以继续寻找入口元素
            if (Objects.equals(fast, slow)) {
//                相遇之后,再生成一个指针,指向头指针,然后原来慢指针和现在新指针同步走动
                Node<T> p = head;
//                快慢指针同步移动,直到相遇
                while (!Objects.equals(p, slow)) {
                    p = p.next;
                    slow = slow.next;
                }
//                找到入口了,结束方法
                return p.elem;
            }
        }
//        没有环,返回null
        return null;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值