链表(单链表,双链表,环形链表)

目录

1.什么是链表

2.单链表的基本结构

3.单链表的增删改查等功能

4.总结


一.什么是链表

        在java中链表是一种数据结构,与数组连续的存储不同,链表在内存中是一种非连续的存储。

通过引用相互连接。

        链表相较于其他数据结构,最大的优势是删除和添加节点(数据)比较方便,尤其是在需要频繁进行这些操作的场景中。

二.单链表的基本结构

        如图:(带头节点的单链表)

链表是由一个一个的节点连接构成的一组线性结构,每一个节点是一个Node类,Node中有一个Node next 来连接下一个节点,尾节点的next=null;

代码:

class Node{
    public int no;            //编号
    public String name;       //姓名
    public String nickname;   //昵称
    public Node next;         //引用
     
    //构造器(可以重载你需要的构造器)
    public Node(int no, String name, String nickname) {
        this.no = no;
        this.name = name;
        this.nickname = nickname;
    }
    
    //toString方法,便于之后能够打印Node
    public String toString() {
        return "Node{" +
                "no=" + no +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}

根据上述代码链表最基本的结构已经完成。

三.单链表的增删改查

因为我所讲述的都是带头结点的单链表,所以先初始化一个头节点。

//初始化一个头节点
    private Node head = new Node(0,"","");

此时我们有仅一个节点的链表

如图:

1.增加新节点

        在单链表方法类中定义一个将数据添加到链表的方法。

        主要步骤是:参数传来新节点,设置一个辅助指针指向头节点。然后遍历到尾节点,让为节点的next指向你要添加的节点。

如图:

代码如下:

public void add(Node newNode){
        Node temp = head;              //设置一个辅助指针指向头节点
        while(true){
              if (temp.next==null){    //遍历到temp.next为null时退出循环
                  break;
              }
              temp = temp.next;        //辅助指针向后移动
            }

        //将最后这个节点的next指向新的节点
        temp.next = newNode;
    }

2.有序增加新节点

     上述增加新节点是将新节点加入到链表末尾,当出现必须有序的需求时,比如Node中有int no这个变量,当需要no值有序插入,则可以换一种添加新节点的逻辑。

        主要逻辑如下:

(1)遍历找到temp.next.no>newNode.no的那个节点,此时temp是需要插入位置的前一个节点。

当然还需要考虑没有比newNode.no还大的值,和newNode.no已经存在的情况。

(2)让newNode.next = temp.next; temp.next = newNode.next;这样就实现了插入。

代码:

    //有序插入节点,插入节点数据不能重复

    public void addByOrder(Node newNode){
        //因为头节点不能动,我们通过辅助指针来找到节点
        //单链表,temp是位于要找节点的前一个位置

        Node temp = head;
        boolean flag = false;    //确认链表中是否存在要添加的顺序
        while (true){
            if (temp.next == null){
                break;
            }
            if (temp.next.no > newNode.no){
                //位置找到了,应该插入到temp后面
                break;
            }else if (temp.next.no == newNode.no){
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag){//编号存在不能添加
            System.out.printf("准备插入的节点编号%d已经存在,不能加入\n",newNode.no);
        }else {
            //插入到链表中,temp的后面(顺序不能调换)
            newNode.next = temp.next;
            temp.next = newNode;
        }
    }

2.修改节点数据

        主要逻辑:通过一个辅助指针遍历找到newNode.no在链表中的编号,然后通过指针来修改该节点的信息。

        

        代码:

//修改节点信息
    public void update(Node newNode){
        //判空
        if (head.next == null){
            System.out.println("链表为空~");
            return;
        }
        //找到要修改的节点,需要no编号
        //定义一个辅助变量
        Node temp = head.next;
        boolean flag = false;
        while (true){
            if (temp == null){
                break;
            }
            if (temp.no == newNode.no){
                flag = true;
                break;
            }
            temp = temp.next;
        }
        //根据flag,判断是否找到要修改的节点
        if (flag){
            temp.name = newNode.name;
            temp.nickname = newNode.nickname;
        }else {
            System.out.printf("没有找到编号%d的节点\n",newNode.no);
        }
    }

3.删除节点

        主要逻辑:传入一个编号参数,遍历找到该参数的前一节点,然后让temp.next = temp.next.next;这样要删除的节点就从链表中断掉。

        如图:

此时no=3的节点的next仍然指向no=4的节点,但是因为没有头节点无法遍历到该节点,说明该节点失效了。

这也是为什么要频繁使用辅助指针来遍历而不直接使用头指针,因为头节点不能改变,它是遍历的起点,如果头节点丢失,链表也就丢失。

代码如下:

//删除指定节点
    public void delete(int no){
        boolean flag = false;
        Node temp = head;
        while (true){
            if (temp.next == null){
                break;
            }if (temp.next.no == no){
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            temp.next = temp.next.next;
        }else {
            System.out.printf("没有找到编号为%d的节点\n",no);
        }
    }

4.查找某个节点

        主要逻辑:传入你要查找的编号,根据编号遍历找到节点,然后输出对应节点的信息。

 代码如下:

//查找
    public void search(int no){
        boolean flag = false;
        Node temp = head;
        while (true){
            if (temp.next == null){
                break;
            }if (temp.next.no == no){
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
            System.out.printf("编号为%d的节点的姓名%s昵称%s\n",no,temp.name,temp.nickname);
        }else {
            System.out.printf("没有找到编号为%d的节点\n",no);
        }
    }

查找倒数第K个节点:

        主要逻辑:先求出链表总长L,因此正数L-K次便是倒数第K个节点。

首先写出求链表总长的方法:

//方法:获取到单链表节点的个数
    public static int getLength(Node head){
        int Length = 0;
        if (head.next == null){
            Length = 0;
        }
        //定义一个辅助指针
        Node cur = head.next;
        while (cur != null){
            length++;
            //遍历
            cur = cur.next;
        }
        return Length;
    }

下面是查找倒数第k个节点的代码:

//找到倒数第K个节点    
    public Node searchK(Node head,int k) {
        int length = SingleLinkedList.getLength(head);   //SingleLinkedList是保存方法的类
        if (k >= length || k <= 0) {
            return null;
        }
        //设置辅助指针
        Node temp = head;
        //遍历次数
        int count = length-k;
        while (true){
            if (count==0){
                break;
            }
            temp = temp.next;
            count--;
        }
        return temp;
    }
        

5.链表翻转

        主要思想:这段代码采用头插法,首先创建一个新的链表头节点,然后遍历原来链表的每一个节点,摘下来后根据头插法插入到新的节点上,就实现了链表翻转。

        头插法特点:每次插入表头的后面。

根据这个方法,我们可以将原来链表一个个取出来,每次取出来放在头节点之后,原链表最后一个取出来的便是新链表的第一个,因此新链表就是原链表的逆序链表。

代码如下:

//链表反转
    public void reverseLinked(Node head){
        if (head.next==null||head.next.next==null){
            return;
        }
        //创建一个新的头节点
        Node endNode = new Node(0,"","");
        //设置辅助指针遍历用于遍历
        HeroNode temp = head.next;
        //用于保存当前节点的下一个节点引用
        HeroNode next = null;
        while (temp!=null){
            next = temp.next;            //保存当前节点的下一节点
            temp.next = endNode.next;    //将当前节点的next指向新链表的头部的next
            endNode.next = temp;         //新链表头节点指向当前节点
            temp = next;                 //辅助指针指向后一节点
        }
        head.next = endNode.next;        //将原链表头节点下一节点指向新链表的头节点后一个
    }

 6.普通打印链表

        基本原理:遍历到temp.next==null即可退出,每次以此输出temp指向的节点。

代码如下:

//显示链表
    public void list(){
        if (head.next ==null){
            System.out.println("链表为空");
            return;
        }
        //因为头节点不能动,因此我们需要一个辅助变量(指针)
        Node temp = head.next;
        while(true){
            //判断是否到链表最后
            if (temp == null){
                break;
            }
            System.out.println(temp);    //因为重写了toString方法
            //后移
            temp = temp.next;
        }
    }

7.逆序打印

基本原理:逆序打印可以先实现逆序翻转再遍历打印,但是这样就改变了链表的结构,可以通过栈的特性:先进后出原则,先将链表压入一个栈中,再一个一个出栈输出即可实现逆序打印。

如图:

出栈的顺序是从上面出,依次向下。所以出栈的顺序是cba,进栈顺序是abc,实现了逆序。

代码如下:

//利用栈将各个节点压入栈中,然后利用栈的先进后出特点,实现逆序打印的效果
    public static void reversePrint(Node head){
        if (head.next==null){
            System.out.println("空链表不能打印");
            return;
        }
        //创建一个栈,将各个节点压入栈中
        Stack<Node> stack = new Stack<>();
        Node cur = head.next;
        while (cur!=null){
            stack.push(cur);//将cur指向的节点进栈
            cur = cur.next;//让cur后移,能够压入下一个节点
        }
        while (stack.size()>0){
            System.out.println(stack.pop());    //循环出栈,直到栈空
        }
    }
}

8.合并两个链表

如图:

根据所给的信息我们提出解题思路:

设置一个新的头节点,分别设置一个赋值节点current给这个新头节点,然后循环遍历比较值的大小,小的则连接到新头节点后面并且向后走一位,直到有一个链表为空。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode list = new ListNode(0);    //设置新头节点
        ListNode current = list;            //新头节点的的辅助指针
        while(list1!=null&&list2!=null){
            if(list1.val>=list2.val){       //比较两个链表的值大小
                current.next = list2;       //list2比较小则连接到新链表上
                list2 = list2.next;         //list2向后移动
            }else{
                current.next = list1;       //同理
                list1 = list1.next;
            }
            current = current.next;
        }
        if(list1!=null){                    //如果list2先遍历完,则list1剩下连接在后面
            current.next=list1;
        }else{                              //同理
            current.next=list2;
        }
        return list.next;                   //返回新链表
    }
}

四:总结

根据链表的特性和链表的增删改查,想要熟练运用链表,就是要熟练修改引用(指针)的指向。当然遇到比如让链表逆序和逆序打印(不改变初始结构时),有更便捷的方法,头插法和压栈法。总体来说最重要的还是熟练掌握引用的修改以灵活使用链表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值