Java数据结构与算法:单向链表、增删改查、求单链表有效节点个数、查找单链表倒数第k个节点、单链表反转、从尾到头打印单链表、合并两个有序链表后依然有序


单向链表

单链表示意图:
在这里插入图片描述

  • 链表是以节点的方式存储,链式存储
  • 每个节点包含data域(用于存储该节点中的数据) 和 next域(用于存储下一个节点对象)
  • 各个节点不一定连续存储
  • 链表分带头节点和不带头节点两种,根据实际需求来确定

1、单链表的 增 删 改 查 思路分析

在这里插入图片描述

头节点不存任何数据,只用作链表的头,方便对链表增删改查等。

增加插入新节点

在这里插入图片描述

如果要将新节点插入到data1和data2之间,那么就找到插入位置,然后将data1的next指向newdata,newdata的next指向data2,这样就完成了添加插入。

如果想要把新节点插入到尾节点之后,直接将尾节点的next指向新节点即可。

删除节点

在这里插入图片描述

删除节点,比如这里要删除数据域为data2这个节点,将data2这个节点的前节点的next指向data2这个节点的next节点即可。由于data2这个节点没有任何引用,GC垃圾回收机制会处理这样的垃圾对象。

修改节点内容

通过遍历,找到需要更改的节点,对其内容更改即可。

查询节点

通过遍历,查询所有节点,查询指定节点。


2、代码实现

实现很简单,就判断id值与下一个节点的id值的大小,如果添加节点的id小于判断的下一个节点的id,那么就在这个节点的前面插入新添加的节点。需要注意的是,需要判断链表中是否已存在该节点,这就避免了重复添加节点。

按顺序插入节点到指定位置。

代码

public class SingleLinkedListDemon {
    public static void main(String[] args) {
        SingleLinkedList singleLinkedList = new SingleLinkedList();
//        singleLinkedList.addNode(new Node(1, "heroC","20"));
//        singleLinkedList.addNode(new Node(3, "yikeX","21"));
//        singleLinkedList.addNode(new Node(2, "wenxC","18"));
        singleLinkedList.addNodeById(new Node(1, "heroC","20"));
        singleLinkedList.addNodeById(new Node(3, "yikeX","21"));
        singleLinkedList.addNodeById(new Node(2, "wenxC","18"));
        singleLinkedList.addNodeById(new Node(2, "wenxC","18"));
        singleLinkedList.showNode();

        singleLinkedList.updateNode(new Node(4, "wenxC~~","20"));
        // 修改id为2的节点
        System.out.println("修改id为2的节点");
        singleLinkedList.updateNode(new Node(2, "wenxC~~","20"));

        singleLinkedList.delNode(3);

        singleLinkedList.showNode();
    }
}

class SingleLinkedList{
    private final Node head = new Node(); // 创建一个头节点

    // 向链表中添加节点,直接在尾节点后添加新节点
    public void addNode(Node node){
        Node temp = head; // 获取头节点
        while (true){
            // 判断当前节点的next是否指向节点对象,如果为null,说明当前节点是尾节点
            if(temp.next == null){
                // 向尾节点的next添加新节点
                temp.next = node;
                break;
            }
            // 如果不是尾节点,那么就next到指向到下一个节点对象
            temp = temp.next;
        }
    }

    // 优化添加方式,根据id大小添加元素到指定位置
    public void addNodeById(Node node){
        Node temp = head;
        boolean flag = false;
        while (true){
            if(temp.next == null){
                // 说明是temp是尾节点,退出此时的temp就是需要往后添加node的节点
                break;
            }
            if(temp.id == node.id){
                // id一样,说明链表中已添加了同id的节点对象
                // flag为true,说明已存在同样id的节点
                flag = true;
                break;
            }else if(temp.next.id > node.id){
                // 如果temp下一个节点对象的id大于添加节点的id,那么这个添加的节点
                // 就应该插入到temp节点的后面,temp节点的下一个节点的前面
                // 退出此时的temp就是需要往后添加node的节点
                break;
            }
            temp = temp.next; // 以上都不满足,就继续判断下一个节点
        }
        if(flag){
            System.out.println("编号:"+ node.id + " 已在链表中存在,无法添加到链表中...");
        }else {
            // 插入节点操作
            node.next = temp.next;
            temp.next = node;
        }
    }

    // 删除节点
    public void delNode(int id){
        Node temp = head;
        boolean flag = false;
        while (true){
            if(temp.next == null){
                flag = true;
                break;
            }
            if(temp.next.id == id){
                break;
            }
            temp = temp.next;
        }
        if(flag){
            System.out.println("链表中无id为"+id+"的节点...");
        }else {
            System.out.println("已删除id为"+temp.next.id+"的节点");
            temp.next = temp.next.next;
        }
    }

    // 修改节点内容
    public void updateNode(Node node){
        Node temp = head.next;
        while (true){
            if(temp == null){
                System.out.println("链表中无id为"+node.id+"的节点,无法修改...");
                break;
            }
            if(temp.id == node.id){
                temp.name = node.name;
                temp.age = node.age;
                break;
            }
            temp = temp.next;
        }
    }

    // 遍历链表中的所有节点
    public void showNode(){
        Node temp = head.next; // 获取头节点的下一个节点
        if(temp == null){ // 如果头节点的下一个节点为null,说明是空链表
            System.out.println("链表为空...");
            return;
        }
        while (true){
            System.out.println(temp.toString());
            temp = temp.next;
            if(temp == null){
                break;
            }
        }
    }


}

// 创建一个节点对象
class Node{
    // data域 ,在真实开发中应封装为一个类
    public int id;
    public String name;
    public String age;
    // next域
    public Node next; // 用于存储下一个节点对象(指向下一个节点)

    public Node() {
    }

    public Node(int id, String name, String age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Node{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}

结果

编号:2 已在链表中存在,无法添加到链表中...
Node{id=1, name='heroC', age='20'}
Node{id=2, name='wenxC', age='18'}
Node{id=3, name='yikeX', age='21'}
链表中无id为4的节点,无法修改...
修改id为2的节点
已删除id为3的节点
Node{id=1, name='heroC', age='20'}
Node{id=2, name='wenxC~~', age='20'}

3、练习题

public class SingleLinkedListTest {
    public static void main(String[] args) {
        SinLinkedList sinLinkedList = new SinLinkedList();
        sinLinkedList.addNode(new SinNode(3));
        sinLinkedList.addNode(new SinNode(1));
        sinLinkedList.addNode(new SinNode(4));
        sinLinkedList.addNode(new SinNode(2));
        sinLinkedList.showNode();

        
        SingleLinkedListTest test = new SingleLinkedListTest();
        // 测试第一题:
        System.out.println("单链表中有效个数:"+test.getLinkedListLength(sinLinkedList.getHead()));
        // 测试第二题:
        System.out.println("倒数第2个节点:"+test.getSinNode(sinLinkedList.getHead(),2));
        // 测试第三题:
        test.reverseSinNode(sinLinkedList.getHead());
        System.out.println("链表反转:");
        sinLinkedList.showNode();
        // 测试第四题:
        System.out.println("倒叙打印链表:");
        test.reversePrintNode(sinLinkedList.getHead());
        // 测试第五题:
        System.out.println("第一个有序链表:");
        SinLinkedList sinLinkedList1 = new SinLinkedList();
        sinLinkedList1.addNode(new SinNode(2));
        sinLinkedList1.addNode(new SinNode(5));
        sinLinkedList1.addNode(new SinNode(6));
        sinLinkedList1.addNode(new SinNode(9));
        sinLinkedList1.showNode();

        System.out.println("第二个有序链表:");
        SinLinkedList sinLinkedList2 = new SinLinkedList();
        sinLinkedList2.addNode(new SinNode(1));
        sinLinkedList2.addNode(new SinNode(3));
        sinLinkedList2.addNode(new SinNode(7));
        sinLinkedList2.addNode(new SinNode(12));
        sinLinkedList2.showNode();

        System.out.println("合并结果:");
        SinNode sinNode = test.merge(sinLinkedList1.getHead(), sinLinkedList2.getHead());
        if(sinNode!=null){
           while (sinNode.next!=null){
               System.out.println(sinNode.next);
               sinNode = sinNode.next;
           }
        }
    }

    // 第一题:代码
    
    // 第二题:代码
    
    // 第三题:代码
    
    // 第四题:代码
    
    // 第五题:代码
}

class SinLinkedList{
    private final SinNode head = new SinNode();

    public SinNode getHead() {
        return head;
    }

    public void addNode(SinNode node){
        SinNode temp = head;
        boolean flag = false;
        while (true){
            if(temp.next == null){
                break;
            }
            if(temp.next.id > node.id){
                break;
            }else if(temp.next.id == node.id){
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if(flag){
            System.out.println("链表中已有该节点...");
        }else {
            node.next = temp.next;
            temp.next = node;
        }
    }

    public void showNode(){
        SinNode temp = head.next;
        while (true){
            if(temp==null){
                break;
            }else {
                System.out.println(temp);
            }
            temp = temp.next;
        }
    }
}


class SinNode{
    public int id;
    public SinNode next;

    public SinNode() {
    }

    public SinNode(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "SinNode{" +
                "id=" + id +
                '}';
    }
}

# 1求单链表中有效节点个数

思路:直接遍历单链表,并通过变量记录有效节点数

public int getLinkedListLength(SinNode head){
    SinNode temp = head;
    int length = 0;
    while (true){
        if(temp.next == null){
            return length;
        }
        length++;
        temp = temp.next;
    }
}

# 2查找单链表中倒数第k个节点

(新浪)思路:因为单链表只能从头遍历到尾,要找到倒数第k个节点,就先得遍历一遍链表得到链表长度size,因此要得到倒数第k个节点就是找到size-k位置的节点即可。从head节点的next节点开始计算,那么找到倒数第k个基点就是需要移动size-k次。

// 获得倒数第k个节点
public SinNode getSinNode(SinNode head, int k){
    SinNode temp = head.next;
    int size = getLinkedListLength(head); // 获得有效节点数
    if(temp == null){
        return null;
    }
    if(k<=0 || k>size){
        return null;
    }
    for (int i = 0; i < size-k; i++) {
        temp = temp.next;
    }
    return temp;
}

# 3单链表的反转

(腾讯)思路:新定义一个链表用于存储反转后的链表,遍历原链表,将原链表的每一个节点都依次插入到新链表的头部的next节点上。最后将原链表指向新链表就完成了反转。

// 链表反转
public SinNode reverseSinNode(SinNode head){
    // 当链表为空或者链表中只有一个节点的时候,直接返回
    if(head.next == null || head.next.next == null){
        return head;
    }

    SinNode reverseNode = new SinNode();
    SinNode currentNode = head.next;
    SinNode curNextNode;
    // curNextNode用于记录当前节点的下一个节点,为了保证链表不断裂,能够持续遍历。
    // 因当前节点加到新链表中,要想将原链表的当前节点的下一个节点继续添加到新链表的头部,就必须记录下来

    while (currentNode != null){
        // 先把当前节点的下一个节点记录下来
        curNextNode = currentNode.next;
        // 把当前节点的下一个节点连接到反转链表头部指向的下一个节点,
        // 这样反转链表除了头部其余节点都在当前节点后面了
        currentNode.next = reverseNode.next;
        // 将反转链表的头部指向当前节点,形成新的反转链表,此时当前节点已成功从反转链表头部插入
        reverseNode.next = currentNode;
        // 最后将之前记录的原链表的当前节点的下一个节点赋值给当前节点,成为需要添加到反转链表的节点
        currentNode = curNextNode; 
    }
    head.next = reverseNode.next; // 将原链表指向反转后的链表
    return head;
}

# 4从尾到头打印链表

(百度)思路:遍历链表,以此将每个节点压入栈(先进后出)中,最后遍历栈即可。

// 从尾到头打印链表
public void reversePrintNode(SinNode head){
    if(head.next == null){
        return;
    }
    SinNode temp = head.next;
    Stack<SinNode> stack = new Stack<>();
    while (temp!=null){
        stack.push(temp);
        temp = temp.next;
    }
    while (!stack.isEmpty()){
        System.out.println(stack.pop());
    }
}

# 5合并两个有序链表,合并之后链表依然有序,并返回合并后的链表

思路:定义一个新的链表,两个有序链表在加入到新链表中时,比较节点大小,小的先加入即可

// 合并两个有序链表并返回这个合并的链表
public SinNode merge(SinNode head1, SinNode head2){
    if (head1.next == null){
        return head2;
    }else if (head2.next == null){
        return head1;
    }

    SinNode merge = new SinNode();
    SinNode tempMerge = merge; // 1
    SinNode temp1 = head1.next;
    SinNode temp2 = head2.next;

    while (true){
        if(temp1==null && temp2==null){
            break;
        }else if(temp1==null){
            tempMerge.next = temp2;
            break;
        }else if (temp2 == null){
            tempMerge.next = temp1;
            break;
        }

        if(temp1.id < temp2.id){
            tempMerge.next = temp1;
            temp1 = temp1.next;
        }else{
            tempMerge.next = temp2;
            temp2 = temp2.next;
        }
        tempMerge = tempMerge.next;
    }
    return merge;
}

4、对辅助节点的理解

  • 写第5题时,突然有了疑惑。为什么必须要1编号位置的代码,辅助节点?

    ​ 这个辅助变量是用于遍历或者对链表操作而用的,这个变量的指向一直在改变,如果不需要辅助节点,直接使用这里的merge变量去操作,也是可以将两个有序链表合并,但是返回的是merge,这时候返回的就是merge被定位的位置节点,就不能返回一个完整的链表。如果借用辅助节点,对merge对象进行操作,由于merge的角色一直没有变,一直是头节点,返回的也就是头节点,这样就能愉快的使用整个链表的数据。

  • 为什么第三题,新链表reverseNode不需要辅助节点?

    ​ 很简单,因为向reverseNode链表中添加的时候,一直是从头节点添加节点,reverseNode的角色也是一直没有变,一直是头节点,返回的也就是头节点。但也可以使用辅助节点。

总之,要确保返回的是头节点,这样才能愉快的使用或获取整个单向链表的数据。


5、单链表的缺点

  • 查找方向只能是一个放向
  • 不能实现自我删除,需要借助辅助节点
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值