Java实现单向链表及其常见操作

本文将使用Java实现链表以及链表的常用操作。

一、什么是链表

    链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成,而且由于没有闲置的内存,因此空间效率比数组高。其插入操作可达到O(1)复杂度,但是查找或者访问特定的结点复杂度是O(n)。

二、单向链表的元素增加实现

    在使用Java实现链表之前需要搞懂,为什么Java可以实现链表。在C/C++中链表的实现是通过指针来实现,那在Java中呢?实际上在Java中除了基本数据类型到处都是指针,比如MyClass mc = new MyClass();实际上mc就相当于指向内存中MyClass对象的指针。因此可以利用这个特性实现链表:

package link;

public class MyLink<E> {
    private LinkNode<E> head;
    private static class LinkNode<E>{//使用泛型保证程序的可重用性和安全性
        E data;
        LinkNode<E> next;//指向下一个结点的指针
    }
    
    //添加新的结点到尾部
    public void add(E data){
        LinkNode<E> newNode = new LinkNode<E>();
        newNode.data = data;
        newNode.next = null;
        if(head == null){//之前的链表为空
            head = newNode;//新结点即为头结点
        }
        else{
            LinkNode<E> temp = head;
            while(temp.next!=null){
                temp = temp.next;
            }//找到最后一个结点
            temp.next = newNode;//最后一个结点指向新结点
        }
    }
    //重写toString 方便打印链表
    @Override
    public String toString(){
        if(head==null)
            return "Empty link";
        LinkNode<E> temp = head;
        StringBuilder sb = new StringBuilder();
        while(temp.next!=null){
            sb.append(temp.data+" ");
            temp = temp.next;
        }
        sb.append(temp.data);
        return sb.toString();
    }
}


    请特别留意头指针为空的时候的处理。
三、单向链表的元素删减实现

    1、一般思路下,如果想要删除data值对应的结点,我们需要O(n)的时间复杂度来找到这个结点的上一个结点,然后将需要删除的结点上一个结点的next指针指向需要删除结点的下一个结点,以此跳过被删除结点,进行删除,我们先来实现这种思路:

    public boolean removeNode(E data){
        if(head==null)
            return false;
        LinkNode<E> temp = head;
        if(temp.data == data){//头结点就是需要删除的元素
            head = temp.next;//直接把头结点指向原先头结点的下一个结点
            //由于原先的头结点不再能够被访问到,所以自动gc
            return true;
        }
        else{
            while(temp.next!=null&&temp.next.data!=data){
                temp = temp.next;
            }
            if(temp.next.data==data){//说明找到了待删除结点 其上一个结点是temp
                temp.next = temp.next.next;//将待删除结点的上一个结点的next指针指向被删除元素的next指针指向的结点
                //被删除元素已经无法被访问到 自动gc
                return true;
            }
            else{
                return false;//没找到待删除元素
            }
        }
    }


    这种思路需要注意:由于需要查找的是被删除的元素的上一个元素,因此需要对头结点做特殊处理判断。

    2、上面这种删除思路的时间复杂度是O(n),因为我们需要找到被删除结点的上一个结点,所以需要我们花费O(n)的时间复杂度去查找该节点,我们能不能实现一种删除,使得已知一个被删除结点,就能直接删除,从而使得时间复杂度达到O(1)呢?

    答案当然是可以的,设想我们现在有以下的链表:a->b->c->d,如果我们需要删除结点b,我们可以把结点c复制到结点b,然后把把复制之后的结点b(现在变成了结点c)的next指针指向结点c的next指针指向的结点(也就是结点d),这样便很巧妙地在O(1)复杂度删除了所需要删除的结点。

    这种思路需要注意,对于长度为n的链表,前n-1个结点可以在O(1)时间复杂度进行删除,然而对于尾结点就不能这样,我们还是需要顺序查找,因此对于尾结点的删除还是需要O(n)时间复杂度。但是平均时间复杂度是[(n-1)*O(1)+O(n)]/n,结果是O(1),因此此算法的时间复杂度仍然是O(1)。

    此算法基于两个假设:被删除的结点需要确保在链表内;传入的参数是被删除结点而不是被删除结点对应的值。下面我们来实现这个算法:

    public boolean removeNode2(LinkNode<E> node){
        if(node==null||head==null){//头结点为空或者待删除结点为空直接返回false
            return false;
        }
        if(node.next!=null){//待删除结点不是尾结点
            LinkNode<E> nextNode = node.next;
            //将被删除结点的下一个结点复制给被删除结点
            node.data = nextNode.data;
            node.next = nextNode.next;
            //被删除结点的下一个结点由于没有指针指向它 被GC
        }
        else if(head == node){//链表只有一个结点 而且恰好是需要被删除的结点
            head = null;
        }
        else{//链表中有多个结点 需要删除尾结点 这种情况需要遍历链表
            LinkNode<E> temp = head;
            while(temp.next.next!=null){
                temp = temp.next;
            }//找到尾结点的前一个结点
            temp.next = null;
            //尾结点由于没有指针指向它 被GC
        }
        return true;
    }


四、单向链表的反向打印

    说到单向链表的反向打印,可能很多人会想到链表的倒置,但是反向打印会改变原链表的结构实际上是不合理的。反向打印很明显是一个先进后出的结构,也就是我们可以借用栈来实现这个需求:

    public void oppositeDisplay(){
        Stack<E> stack = new Stack<E>();
        LinkNode<E> temp = head;
        while(temp!=null){
            stack.push(temp.data);
            temp = temp.next;
        }
        StringBuilder sb = new StringBuilder();
        while(!stack.isEmpty()){
            sb.append(stack.pop()+" ");
        }
        System.out.println(sb.toString());
    }


    既然我们使用了栈,那么我们能想到递归实际上本质就是一个栈结构,因此我们也可以使用递归来实现,每当访问到一个结点,先访问其后面的结点,然后再访问它,但是由于栈空间有一定深度限制和效率影响,上面的方法固然比下面这种要好:

    public void oppsiteDisplay2(){//只暴露出这个方法
        oppsiteDisplayWithRecursive(head);
    }
    private void oppsiteDisplayWithRecursive(LinkNode<E> temp){
        if(temp!=null){
            if(temp.next!=null){
                oppsiteDisplayWithRecursive(temp.next);
            }
            System.out.print(temp.data+" ");
        }
    }


五、获得单向链表倒数第i个结点

    这里我们把倒数第1个结点视为尾结点,正着数的第0个结点视为头结点,看到这个需求,我们脑子里可能很快会蹦出一种很简单的思路,先遍历第一次链表,获得该链表的长度n,该链表的倒数第i个结点实际上就是正着数的第n-i个结点。然而这种思路会遍历链表两次,显然我们需要寻找更好的解法,这里就不实现这种思路了。

    现在讲解第二种思路,我们可以用两个指针遍历该链表,第一个指针遍历到第i-1个结点的时候,第二个指针开始和第一个指针一起遍历链表,当第一个指针遍历到链表尾结点的时候,第二个指针就是倒数第i个结点。我们来实现这个算法:

    public LinkNode<E> findNodeToTail(int indexToTail){
        if(indexToTail<=0)//异常参数直接返回null
            return null;
        LinkNode<E> fastNode = head;
        LinkNode<E> slowNode = head;
        int i = 0;
        while(fastNode!=null&&i<indexToTail-1){
            fastNode = fastNode.next;
            i++;
        }
        if(fastNode == null)//indexToTail大于等于链表的长度 返回null
            return null;
        while(fastNode.next!=null){
            fastNode = fastNode.next;
            slowNode = slowNode.next;
        }
        return slowNode;
    }


    这里请注意代码的鲁棒性,indexToTail这个参数小于0或者大于等于链表的长度如果不做特殊处理都会导致程序崩溃,一定要考虑周全。
六、单向链表的链表反转

    我们还是先来思考这个问题如何解决,对于链表a->b->c->d我们想要反转,首先对于头结点a,我们可以知道它的前一个结点为null,后一个结点为b,访问到结点b的时候,b的前一个节点为a,后一个结点为c。我们先把a,b进行反转,链表变成a<-b c->d,链表从b,c处断开了,因此我们在反转一个结点之前的所有结点的时候需要记录该结点,该结点的前一个结点,该结点的后一个结点,然后挨个反转。下面我们来实现这个算法:

    public void reverse(){
        if(head == null||head.next == null){//当链表只有一个结点或者没有结点的时候 直接结束
            return;
        }
        LinkNode<E> node = head;
        LinkNode<E> nextNode = null;
        LinkNode<E> prevNode = null;
        while(node!=null){
            nextNode = node.next;//先记录当前结点的下一个结点
            if(nextNode == null)//遍历到了尾结点 这个时候尾结点就是新的头结点
                head = node;
            node.next = prevNode;//反转当前结点和前一个结点
            prevNode = node;
            node = nextNode;
        }
    }


    本算法的关键之处在于记录结点的前一个结点和后一个结点,防止链表反转之后断裂,无法继续反转。另外由于需要记录结点的后一个结点,因此请注意代码的鲁棒性,对链表无结点的情况做特殊处理。
七、合并两个有序的单向链表
    两个链表既然是有序的,那么我们可以使用两个指针分别遍历两个链表,每次取出小的一方,放在新的链表的首部,然后继续之前的操作,我们来进行实现:

    @SuppressWarnings("unchecked")
    public void merge(MyLink<E> link1, MyLink<E> link2){
        LinkNode<E> head1 = link1.head;
        LinkNode<E> head2 = link2.head;
        LinkNode<E> tail = null;
        while(head1!=null&&head2!=null){
            if(((Comparable<E>)head1.data).compareTo((E)head2.data)<0){
                add(head1.data);
                tail = head1;
                head1 = head1.next;
            }
            else{
                add(head2.data);
                tail = head2;
                head2 = head2.next;
            }
        }
        if(head1!=null){
            while(head1!=null){
                add(head1.data);
                head1 = head1.next;
            }
        }
        else if(head2!=null){
            while(head2!=null){
                add(head2.data);
                head2 = head2.next;
            }
        }
    }
    同样,注意代码的鲁棒性,注意链表为空的时候的特殊处理。

八、判断单向链表有没有环

    判断单向链表有没有环很简单,用一个快指针和一个慢指针,如果存在环,快指针和慢指针一定会相遇,否则不会:

 //慢指针和快指针如果相遇说明有环
    public static<E> boolean judgeRing(MyLink<E> link){
        LinkNode<E> slow = link.head;
        LinkNode<E> fast = link.head;
        while(fast!=null&&fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast)
                return true;
        }
        return false;
    }

九、获取单向链表环的长度

    如果存在环,从上面方法的碰撞点开始,快指针和慢指针第二次相遇经过的慢指针移动次数就是环的长度:

    //记录下碰撞点 快指针满指针从该点开始再次碰撞走过路程就是环的长度
    public static<E> int findRingDis(MyLink<E> link){
        LinkNode<E> slow = link.head;
        LinkNode<E> fast = link.head;
        int dis = -1;
        boolean isMeetFirstTime = true;
        while(fast!=null&&fast.next!=null){
            slow = slow.next;
            fast = fast.next.next;
            if(!isMeetFirstTime)//不是第一次相遇就加距离
                dis++;
            if(slow == fast&&isMeetFirstTime)//第一次相遇后把相遇位置为false
                isMeetFirstTime = false;
            else if(slow == fast && !isMeetFirstTime)
                return dis;
        }
        return -1;
    }

十、获取单向链表环的连接点

    碰撞点到连接点的距离等于头指针到连接点的距离:

    //碰撞点到连接点的距离等于头指针到连接点的距离
    public static<E> LinkNode<E> getConnectNode(MyLink<E> link){
        LinkNode<E> slow = link.head;
        LinkNode<E> fast = link.head;
        
        while(fast!=null&&fast.next!=null){
            if(slow == fast){
                LinkNode<E> impactNode = new LinkNode<E>();
                slow = link.head;
                while(slow!=impactNode){
                    slow = slow.next;
                    impactNode = impactNode.next;
                }
                return slow;
            }
        }
        return null;//没有环当然不存在连接点
    }

十一、获取单向链表环的长度

    代码略,前面都分析出了连接点的位置和环的长度,当然也就得出了单向链表环的长度。

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值