数据结构与算法笔记(二)

295 阅读6分钟

3. 链表

3.1 应用场景和介绍

生活中,火车是一节连着一节,由火车头连接后面的车厢

火车可以存储物资,对应到程序中,就是链表(Linked List)这种数据结构,同样用来存储数据

链表:有序不连续的链式结构

有序指的是我们可以通过每一个节点,找到下一个节点,每个节点之间都是有顺序的

不连续指的是在内存中,不像是数组一样,通过一块连续的内存空间来存储数据,如下图所示

链表的特点:

  • 链表是有序不连续的数据结构

  • 每个节点(node)至少有两个域(属性),一个data 域用来保存数据,一个next 域用来指向下一个节点的位置

  • 链表通常会有链表头节点,类似火车头一样,不存储数据,只用来记录位置

  • 单链表带头节点的逻辑结构示意图如下

应用场景:

我们使用链表来完成水浒英雄的crud 操作

3.2 代码思路和示例

先定义一个HeroNode 类,用来保存数据和指向下一个节点的地址,然后定义一个LinkedList,拥有一个 Head 节点,不保存数据,只记录链表的首位置

//  节点对象
class HeroNode {
    int id;
    String name;
    String nickName;
    HeroNode next;

    public HeroNode(int id, String name, String nickName) {
        this.id = id;
        this.name = name;
        this.nickName = nickName;
    }

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

    public HeroNode() {
    }
}

class SingleLinkedList {
    //    头节点,在链表中不能进行操作,只记录
    private HeroNode head = new HeroNode();

    public SingleLinkedList() {
    }
 }
  1. 当我们添加节点的时候,一般有两种方式,第一种是直接添加到链表的尾部,一种是按照Node内部的顺序来进行添加

    第一种方式:直接添加到链表的尾部

示例代码:

    //    默认每次添加节点到链表的末尾
    public void add(HeroNode node) {
        HeroNode temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = node;
    }

第二种方式:按照 HeroNode 的id 来进行添加,也就是二号节点一定要添加到一号节点和三号节点之间,如果当前链表已经有了二号节点,则提示添加失败(通过找到待添加节点的上一个节点来进行添加)

示例代码:

    //    按照顺序插入节点
    public void addByOrder(HeroNode node) {
        HeroNode temp = head;
        boolean flag = false;
        while (true) {
            // 循环到链表最后一个节点,表示待插入的 node的id 大于当前链表的所有node 的id,所以待插入的node应该插入到该链表的末尾
            if (temp.next == null) {
                break;
            }
//            找到了待插入的node 的位置(即找到待插入node的前一个node位置即可)
            if (temp.next.id > node.id) {
                break;
            }
//            表示待插入的node 位置已经有node了,无法插入
            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;
        }
    }
  1. 显示所有节点信息,通过遍历完成

    示例代码

    public void list() {
            HeroNode temp = head;
    
            while (temp.next !=null){
                System.out.println(temp.next);
                temp = temp.next;
            }
    
        }
    
  2. 根据id找到节点并修改节点数据

        public void update(HeroNode newNode) {
            HeroNode temp = head;
    //        用来标记是否找到在链表中找到该节点
            boolean flag = false;
            while (true) {
                if (temp.next == null) {
                    break;
                }
    //            根据id找到该节点
                if (temp.id == newNode.id) {
                    flag = true;
                    break;
                }
                temp = temp.next;
            }
            if (flag) {
    //            修改该节点数据
                temp.name = newNode.name;
                temp.nickName = newNode.nickName;
            } else {
                System.out.println("没找到对应的节点,无法修改");
            }
    
        }
    
  3. 根据节点移除链表中的该节点

    public void remove(HeroNode node) {
        HeroNode temp = head;
        boolean flag = false;
        while (true) {
            if (temp.next == null) {
                break;
            }
        // 找到待移除节点的上一个节点的位置即可    
            if (temp.next.id == node.id) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
//        找到了对应id的node节点,将该node 的上一个node的next 指向该node的next即可
        if (flag) {
            temp.next = temp.next.next;
        } else {
            System.out.println("没有找到对应的节点,无法删除");
        }
    }

3.3 完整代码和测试

public class SingleLinkedList_03 {

    public static void main(String[] args) {

        SingleLinkedList singleLinkedList = new SingleLinkedList();
        HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode node3 = new HeroNode(3, "吴用", "智多星");
        HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
        singleLinkedList.add(node1);
        singleLinkedList.add(node2);
        singleLinkedList.add(node3);
        singleLinkedList.add(node4);

        singleLinkedList.list();

        System.out.println("----------------------------------");
        SingleLinkedList singleLinkedList2 = new SingleLinkedList();
        singleLinkedList2.addByOrder(node1);
        singleLinkedList2.addByOrder(node4);
        singleLinkedList2.addByOrder(node3);
        singleLinkedList2.addByOrder(node2);
        singleLinkedList2.list();
        singleLinkedList2.addByOrder(node3);

        System.out.println("**********************************");

        singleLinkedList2.update(new HeroNode(5, "鲁智深", "花和尚"));
        singleLinkedList2.update(new HeroNode(3, "小吴", "智多星~~~"));
        singleLinkedList2.list();

        System.out.println("**********************************");
        singleLinkedList2.remove(new HeroNode(1, "宋江", "及时雨"));
        singleLinkedList2.remove(new HeroNode(4, "林冲", "豹子头"));
        singleLinkedList2.remove(new HeroNode(5, "鲁智深", "花和尚"));
        singleLinkedList2.list();


    }
}

class SingleLinkedList {
    //    头节点,在链表中不能进行操作
    private HeroNode head = new HeroNode();

    public SingleLinkedList() {
    }

    //    默认每次添加节点到链表的末尾
    public void add(HeroNode node) {
        HeroNode temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = node;
    }

    public void remove(HeroNode node) {
        HeroNode temp = head;
        boolean flag = false;
        while (true) {
            if (temp.next == null) {
                break;
            }
            if (temp.next.id == node.id) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
//        找到了对应id的node节点,将该node 的上一个node的next 指向该node的next即可
        if (flag) {
            temp.next = temp.next.next;
        } else {
            System.out.println("没有找到对应的节点,无法删除");
        }
    }

    //    按照顺序插入节点
    public void addByOrder(HeroNode node) {
        HeroNode temp = head;
        boolean flag = false;
        while (true) {
            // 循环到链表最后一个节点,表示待插入的 node的id 大于当前链表的所有node 的id,所以待插入的node应该插入到该链表的末尾
            if (temp.next == null) {
                break;
            }
//            找到了待插入的node 的位置(即找到待插入node的前一个node位置即可)
            if (temp.next.id > node.id) {
                break;
            }
//            表示待插入的node 位置已经有node了,无法插入
            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 update(HeroNode newNode) {
        HeroNode temp = head;
//        用来标记是否找到在链表中找到该节点
        boolean flag = false;
        while (true) {
            if (temp.next == null) {
                break;
            }
//            根据id找到该节点
            if (temp.id == newNode.id) {
                flag = true;
                break;
            }
            temp = temp.next;
        }
        if (flag) {
//            修改该节点数据
            temp.name = newNode.name;
            temp.nickName = newNode.nickName;
        } else {
            System.out.println("没找到对应的节点,无法修改");
        }

    }

   public void list() {
        HeroNode temp = head;

        while (temp.next !=null){
            System.out.println(temp.next);
            temp = temp.next;
        }

    }


}


//  节点对象
class HeroNode {
    int id;
    String name;
    String nickName;
    HeroNode next;

    public HeroNode(int id, String name, String nickName) {
        this.id = id;
        this.name = name;
        this.nickName = nickName;
    }

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

    public HeroNode() {
    }
}

3.4 常见的单链表面试题

① 求单链表中有效元素的个数

    //    显示链表中有效节点的个数(不包括head节点),可以判断链表是否为空,更好的做法是维护一个计数器
    public int size() {
        HeroNode temp = head;
        int count = 0;
        while (temp.next != null) {
            count++;
            temp = temp.next;
        }
        return count;
    }

② 查找单链表倒数第k 个元素

    //    获取倒数第 index 个节点
    public HeroNode get(int index) {
        if (index > size()) {
            throw new RuntimeException("没有倒数第" + index + "个元素");
        }

        HeroNode temp = head;
        // 查找倒数第一个其实就遍历到最后一个元素
        for (int i = 0; i < size() - index + 1; i++) {
            temp = temp.next;
        }
        return temp;
    }

③ 单链表的反转

代码思路:

新建一个 HeadNode,用于记录反转后的新链表头位置信息,然后依次将原链表的每个元素(node)添加到 HeadNode 的后面(HeadNode.next = node),在此之前我们需要一个 Node 对象(newTemp)来保存每次HeadNode 原来next 的值,然后将 node.next = newTemp

示例代码:

    //    链表反转
    public void reverse() {

//        新链表的头节点
        HeroNode newHead = new HeroNode();
//        用于暂时保存节点数据
        HeroNode newTemp = new HeroNode();

        while (head.next != null) {
//            首先将新链表头节点指向的节点保存起来
            newTemp = newHead.next;
            // 将新链表的头节点重新指向原链表头指向的节点
            newHead.next = head.next;
            // 原链表头节点指向节点的下个节点(相当于将原链表的元素向前进一位)
            head.next = head.next.next;
            // 新链表头节点指向的节点再指向原来指向的节点
            newHead.next.next = newTemp;
        }
        head = newHead;
    }

④ 从尾到头打印单链表

两种方法:

方法2的示例代码如下:

    //   链表逆序打印(使用栈 stack 这种先进后出的数据结构来完成,并且不会影响原链表的数据结构)
    public void reversePrint() {
        Stack<HeroNode> heroNodeStack = new Stack<>();
        HeroNode temp = head;
        while (temp.next != null) {
            heroNodeStack.add(temp.next);
            temp = temp.next;
        }
        while (heroNodeStack.size() > 0) {
            System.out.println(heroNodeStack.pop());
        }
    }

⑤ 合并两个有序的单链表,合并后的链表同样有序

代码思路:遍历一个链表的每个元素,将该元素按照顺序添加到另一个链表中,需要使用到 addByOrder 方法

示例代码如下:

    //    合并两个有序的单链表,合并后的链表依然有序(在一个链表上按顺序添加另外一个链表的每个元素,会影响原来两个有序链表的结构)
    public void mergeByOrder(SingleLinkedList singleLinkedList) {
        HeroNode temp = singleLinkedList.head.next;
//        用来保存temp的next
        HeroNode temp3 = new HeroNode();

//        不停的将 singleLinkedList 中的每个元素按顺序添加到当前的链表中
        while (temp != null) {
            temp3 = temp.next;
//            将 temp 按照顺序添加到当前链表结构中(temp.next 将会被改变,因此影响到singleLinkedList 的结构)
            this.addByOrder(temp);
            temp = temp3;
        }
    }