20200913 第 4 章 链表

第 4 章 链表

4.1 链表(Linked List)介绍

链表是有序的列表, 但是它在内存中是存储如下

img

  1. 链表是以节点的方式来存储,是链式存储
  2. 每个节点包含 data 域, next 域: 指向下一个节点.
  3. 如图: 发现链表的各个节点不一定是连续存储
  4. 链表分带头节点的链表和没有头节点的链表, 根据实际的需求来确定

img

4.2 单链表的应用实例

使用带 head 头的单向链表实现 – 水浒英雄排行榜管理完成对英雄人物的增删改查操作

第一种方法在添加英雄时, 直接添加到链表的尾部

添加(创建):

  1. 先创建一个head 头节点, 作用就是表示单链表的头
  2. 后面我们每添加一个节点,就直接加入到 链表的最后

遍历:

  1. 通过一个辅助变量遍历,帮助遍历整个链表

代码实现

节点实现:

@Data
public class HeroNode {
    private int no;
    private String name;
    private String nickName;
    private HeroNode next;

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

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

链表实现:

public class HeroLinkedList {
    
    private HeroNode head = new HeroNode(0, "", "");

    //添加节点到单向链表
    //思路,当不考虑编号顺序时
    //1. 找到当前链表的最后节点
    //2. 将最后这个节点的next 指向 新的节点
    public void add(HeroNode heroNode) {
        HeroNode curNode = head;
        while (true) {
            if (curNode.getNext() == null) {
                break;
            }
            curNode = curNode.getNext();
        }
        curNode.setNext(heroNode);
    }

    //显示链表[遍历]
    public void list() {
        HeroNode curNode = head.getNext();
        while (true) {
            if (curNode == null) {
                break;
            }
            System.out.println(curNode);
            curNode = curNode.getNext();
        }
    }

}

测试功能:

public class HeroLinkedListTest {
    public static void main(String[] args) {

        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");

        HeroLinkedList list = new HeroLinkedList();
        list.add(hero1);
        list.add(hero2);
        list.add(hero3);
        list.add(hero4);
        list.list();
    }
}

第二种方式在添加英雄时, 根据排名将英雄插入到指定位置(如果有这个排名, 则添加失败, 并给出提示)

需要按照编号的顺序添加

  1. 首先找到新添加的节点的位置, 是通过辅助变量(指针), 通过遍历来搞定
  2. 新的节点.next = temp.next
  3. 将temp.next = 新的节点

代码实现

添加方法:

    //第二种方式在添加英雄时,根据排名将英雄插入到指定位置
    //(如果有这个排名,则添加失败,并给出提示)
    public void addByOrder(HeroNode heroNode) {
        HeroNode curNode = head;
        boolean existFlag = false;
        while (true) {
            if (curNode.getNext() == null) {
                // 已到达链表尾部
                break;
            }

            if (curNode.getNext().getNo() > heroNode.getNo()) {
                // 找到应插入位置
                break;
            }

            if (curNode.getNext().getNo() == heroNode.getNo()) {
                // 已存在此编号英雄
                existFlag = true;
                break;
            }
            curNode = curNode.getNext();
        }

        if (existFlag) {
            System.out.printf("已存在编号为 %d 英雄,不能添加\n", heroNode.getNo());
        } else {
            // 将节点插入链表
            heroNode.setNext(curNode.getNext());
            curNode.setNext(heroNode);
        }
    }

测试功能:

    public static void main(String[] args) {

        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");

        HeroLinkedList list = new HeroLinkedList();
        // list.add(hero1);
        // list.add(hero4);
        // list.add(hero3);
        // list.add(hero2);
        // list.add(hero3);  xxx,这里不可以插入重复节点,否则会造成链表节点间循环


        list.addByOrder(hero1);
        list.addByOrder(hero4);
        list.addByOrder(hero3);
        list.addByOrder(hero2);
        // list.addByOrder(hero4);

        list.list();
    }

更新节点

更新功能:

	public void update(HeroNode heroNode) {
        HeroNode curNode = head;
        boolean existFlag = false;
        while (true) {
            if (curNode == null) {
                break;
            }
            if (curNode.getNo() == heroNode.getNo()) {
                existFlag = true;
                break;
            }
            curNode = curNode.getNext();
        }
        if (existFlag) {
            curNode.setName(heroNode.getName());
            curNode.setNickName(heroNode.getNickName());
        } else {
            System.out.printf("不存在编号为 %d 的英雄\n", heroNode.getNo());
        }
    }

测试功能:

public static void main(String[] args) {

        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");

        HeroLinkedList list = new HeroLinkedList();
        // list.add(hero1);
        // list.add(hero4);
        // list.add(hero3);
        // list.add(hero2);
        // list.add(hero3);  xxx,这里不可以插入重复节点,否则会造成链表节点间循环


        list.addByOrder(hero1);
        list.addByOrder(hero4);
        list.addByOrder(hero3);
        list.addByOrder(hero2);
        // list.addByOrder(hero4);

        list.list();

        System.out.println("修改英雄信息");
        HeroNode hero33 = new HeroNode(2, "吴用2", "智多星3");
        list.update(hero33);
        list.list();
    }

删除节点

删除功能:

public void delete(int no) {
        HeroNode temp = head;
        boolean existFlag = false;
        while (true) {
            if (temp.getNext() == null) {
                break;
            }
            if (temp.getNext().getNo() == no) {
                existFlag =true;
                break;
            }
            temp = temp.getNext();
        }

        if (existFlag) {
            temp.setNext(temp.getNext().getNext());
        } else {
            System.out.printf("链表中没有编号为 %d 的英雄\n", no);
        }
    }

测试功能:

public static void main(String[] args) {

        HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
        HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
        HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
        HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");

        HeroLinkedList list = new HeroLinkedList();
        // list.add(hero1);
        // list.add(hero4);
        // list.add(hero3);
        // list.add(hero2);
        // list.add(hero3);  xxx,这里不可以插入重复节点,否则会造成链表节点间循环


        list.addByOrder(hero1);
        list.addByOrder(hero4);
        list.addByOrder(hero3);
        list.addByOrder(hero2);
        // list.addByOrder(hero4);

        list.list();

        System.out.println("修改英雄信息");
        HeroNode hero33 = new HeroNode(2, "吴用2", "智多星3");
        list.update(hero33);
        list.list();

        System.out.println("删除英雄信息");
        list.delete(1);
        // list.delete(2);
        // list.delete(3);
        // list.delete(4);
        // list.delete(5);
        list.list();
    }

4.3 单链表面试题

求单链表中有效节点的个数

// 单链表中有效节点的个数
public int size() {
    HeroNode curNode = head.getNext();
    int count = 0;
    while (true) {
        if (curNode == null) {
            break;
        }
        count++;
        curNode = curNode.getNext();
    }
    return count;
}

查找单链表中的倒数第 k 个结点


// 查找单链表中的倒数第 k 个结点
public HeroNode findLastNode(int lastIndex) {
    int size = size();
    if (lastIndex > size) {
        return null;
    }

    HeroNode curNode = head.getNext();
    for (int i = 0; i < size - lastIndex; i++) {
        curNode = curNode.getNext();
    }

    return curNode;
}

单链表的反转

思路:

  1. 先定义一个节点 reverseHead = new HeroNode();
  2. 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端.
  3. 原来的链表的head.next = reverseHead.next
/**
 * 翻转单链表,改变了原链表
 */
public void reverse() {
/*
*   1. 先定义一个节点 reverseHead = new HeroNode();
    2. 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端.
    3. 原来的链表的head.next = reverseHead.next
*/

    HeroNode reverseHead = new HeroNode(0, null, null); // 翻转单链表的头结点
    HeroNode curNode = head.getNext(); // 记录遍历原链表的当前节点
    HeroNode next = null; // 临时变量,记录遍历原链表的下一个节点
    while (true) {
        if (curNode == null) {
            break;
        }
        next = curNode.getNext();   // 记录当前节点的下一个节点

        // 将当前节点插入翻转单链表
        curNode.setNext(reverseHead.getNext());
        reverseHead.setNext(curNode);

        curNode = next; // 当前节点后移,遍历单链表
    }
    head.setNext(reverseHead.getNext());
}

/**
 * 翻转单链表,不改变原链表
 */
public HeroLinkedList reverseList() {
    HeroLinkedList reverseList = new HeroLinkedList();
    HeroNode reverseHead = reverseList.head;

    HeroNode curNode = head.getNext();
    while (true) {
        if (curNode == null) {
            break;
        }

        HeroNode heroNode = new HeroNode(curNode);
        // 将当前节点插入翻转单链表
        heroNode.setNext(reverseHead.getNext());
        reverseHead.setNext(heroNode);

        curNode = curNode.getNext();
    }

    return reverseList;
}

合并两个有序的单链表, 合并之后的链表依然有序

/**
 * 合并两个有序的单链表, 合并之后的链表依然有序
 */
public void joinList(HeroLinkedList list) {
    HeroNode curNode = head;
    HeroNode curNode2 = list.head.getNext();
    if (curNode.getNext() == null) {
        curNode.setNext(curNode2);
        return;
    }
    if (curNode2 == null) {
        return;
    }

    while (true) {
        if (curNode.getNext() == null) {
            break;
        }
        if (curNode2 == null) {
            // 已完全加入
            break;
        }
        if (curNode.getNext().getNo() > curNode2.getNo()) {
            HeroNode heroNode = new HeroNode(curNode2);
            heroNode.setNext(curNode.getNext());
            curNode.setNext(heroNode);
            curNode2 = curNode2.getNext(); // curNode2 后移,遍历
        }
        curNode = curNode.getNext(); // curNode 后移,遍历
    }

    // 如果 curNode2 不为空,说明仍有节点没有加入进来
    if (curNode2 != null) {
        curNode.setNext(curNode2);
    }
}

4.4 双向链表

4.4.1双向链表的操作分析和实现

使用带 head 头的双向链表实现 – 水浒英雄排行榜

管理单向链表的缺点分析:

  1. 单向链表, 查找的方向只能是一个方向, 而双向链表可以向前或者向后查找。
  2. 单向链表不能自我删除, 需要靠辅助节点 , 而双向链表, 则可以自我删除, 所以前面我们单链表删除时节点, 总是找到 temp,temp 是待删除节点的前一个节点

分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现

  1. 遍历 方和 单链表一样,只是可以向前,也可以向后查找
  2. 添加 (默认添加到双向链表的最后)
    1. 先找到双向链表的最后这个节点
    2. temp.next = newHeroNode
    3. newHeroNode.pre = temp;
  3. 修改 思路和 原来的单向链表一样.
  4. 删除
    1. 因为是双向链表,因此,我们可以实现自我删除某个节点
    2. 直接找到要删除的这个节点,比如temp
    3. temp.pre.next = temp.next
    4. temp.next.pre = temp.pre;

双向链表的代码实现

节点类:

@Data
public class DoubleHeroNode {
    private int no;
    private String name;
    private String nickName;

    private DoubleHeroNode next;
    private DoubleHeroNode pre;

    public DoubleHeroNode(int no, String name, String nickName) {
        this.no = no;
        this.name = name;
        this.nickName = nickName;
    }

    public DoubleHeroNode(DoubleHeroNode heroNode) {
        this.no = heroNode.no;
        this.name = heroNode.name;
        this.nickName = heroNode.nickName;
    }

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

双向列表实现:

public class DoubleHeroLinkedList {

    private DoubleHeroNode head = new DoubleHeroNode(0, "", "");

    //添加节点到单向链表
    //思路,当不考虑编号顺序时
    //1. 找到当前链表的最后节点
    //2. 将最后这个节点的next 指向 新的节点
    public void add(DoubleHeroNode heroNode) {
        DoubleHeroNode curNode = head;
        while (true) {
            if (curNode.getNext() == null) {
                break;
            }
            curNode = curNode.getNext();
        }
        curNode.setNext(heroNode);
        heroNode.setPre(curNode);
    }

    public void addByOrder(DoubleHeroNode heroNode) {
        DoubleHeroNode curNode = head;
        boolean existFlag = false;
        while (true) {
            if (curNode.getNext() == null) {
                // 已到达链表尾部
                break;
            }

            if (curNode.getNext().getNo() > heroNode.getNo()) {
                // 找到应插入位置
                break;
            }

            if (curNode.getNext().getNo() == heroNode.getNo()) {
                // 已存在此编号英雄
                existFlag = true;
                break;
            }
            curNode = curNode.getNext();
        }

        if (existFlag) {
            System.out.printf("已存在编号为 %d 英雄,不能添加\n", heroNode.getNo());
        } else {
            // 将节点插入链表

            if (curNode.getPre() == null) {
                // 如果为 head
                heroNode.setNext(curNode.getNext());
                curNode.setNext(heroNode);
                heroNode.setPre(curNode.getPre());
            } else {
                curNode.getPre().setNext(heroNode);
                heroNode.setPre(curNode.getPre());
                curNode.setPre(heroNode);
                heroNode.setNext(curNode);
            }

        }
    }

    public void update(DoubleHeroNode heroNode) {
        DoubleHeroNode curNode = head;
        boolean existFlag = false;
        while (true) {
            if (curNode == null) {
                break;
            }
            if (curNode.getNo() == heroNode.getNo()) {
                existFlag = true;
                break;
            }
            curNode = curNode.getNext();
        }
        if (existFlag) {
            curNode.setName(heroNode.getName());
            curNode.setNickName(heroNode.getNickName());
        } else {
            System.out.printf("不存在编号为 %d 的英雄\n", heroNode.getNo());
        }
    }

    /**
     * 双向链表可以自我删除
     *
     * @param no
     */
    public void delete(int no) {
        DoubleHeroNode tempNode = head.getNext();
        boolean existFlag = false;
        while (true) {
            if (tempNode == null) {
                break;
            }
            if (tempNode.getNo() == no) {
                existFlag = true;
                break;
            }
            tempNode = tempNode.getNext();
        }

        if (existFlag) {
            tempNode.getPre().setNext(tempNode.getNext());
            if (tempNode.getNext() != null) {
                tempNode.getNext().setPre(tempNode.getPre());
            }
        } else {
            System.out.printf("链表中没有编号为 %d 的英雄\n", no);
        }
    }


    //显示链表[遍历]
    public void list() {
        DoubleHeroNode curNode = head.getNext();
        while (true) {
            if (curNode == null) {
                break;
            }
            System.out.println(curNode);
            curNode = curNode.getNext();
        }
    }
    
}

测试功能:

public class DoubleHeroLinkedListTest {
    public static void main(String[] args) {
        // 测试
        System.out.println("双向链表的测试");
        // 先创建节点
        DoubleHeroNode hero1 = new DoubleHeroNode(1, "宋江", "及时雨");
        DoubleHeroNode hero2 = new DoubleHeroNode(2, "卢俊义", "玉麒麟");
        DoubleHeroNode hero3 = new DoubleHeroNode(3, "吴用", "智多星");
        DoubleHeroNode hero4 = new DoubleHeroNode(4, "林冲", "豹子头");
        // 创建一个双向链表
        DoubleHeroLinkedList doubleLinkedList = new DoubleHeroLinkedList();
        doubleLinkedList.add(hero1);
        doubleLinkedList.add(hero2);
        doubleLinkedList.add(hero3);
        doubleLinkedList.add(hero4);

        doubleLinkedList.list();

        // 修改
        DoubleHeroNode newHeroNode = new DoubleHeroNode(4, "公孙胜", "入云龙");
        doubleLinkedList.update(newHeroNode);
        System.out.println("修改后的链表情况");
        doubleLinkedList.list();

        // 删除
        doubleLinkedList.delete(3);
        System.out.println("删除后的链表情况~~");
        doubleLinkedList.list();

        // 按顺序插入
        System.out.println("按顺序插入链表情况~~");
        doubleLinkedList = new DoubleHeroLinkedList();
        DoubleHeroNode hero11 = new DoubleHeroNode(11, "宋江", "及时雨");
        DoubleHeroNode hero21 = new DoubleHeroNode(21, "卢俊义", "玉麒麟");
        DoubleHeroNode hero31 = new DoubleHeroNode(31, "吴用", "智多星");
        DoubleHeroNode hero41 = new DoubleHeroNode(41, "林冲", "豹子头");
        doubleLinkedList.addByOrder(hero11);
        doubleLinkedList.addByOrder(hero31);
        doubleLinkedList.addByOrder(hero41);
        doubleLinkedList.addByOrder(hero21);
        doubleLinkedList.list();
    }
}

4.7 Josephu(约瑟夫) 问题

问题描述

Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。

n = 5 , 即有5个人
k = 1, 从第一个人开始报数
m = 2, 数2下

出圈的顺序
2->4->1->5->3

思路分析

用一个不带头结点的循环链表来处理 Josephu 问题: 先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数, 计到 m 时, 对应结点从链表中删除, 然后再从被删除结点的下一个结点又从 1 开始计数, 直到最后一个结点从链表中删除算法结束。

构建一个单向的环形链表思路

  1. 先创建第一个节点, 让 first 指向该节点,并形成环形
  2. 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可.

遍历环形链表

  1. 先让一个辅助指针(变量) curBoy,指向first节点
  2. 然后通过一个while循环遍历 该环形链表即可 curBoy.next == first 结束

代码实现

节点类:

@Data
public class BoyNode {
    private int no;
    private BoyNode next;

    public BoyNode(int no) {
        this.no = no;
    }

    @Override
    public String toString() {
        return "BoyNode{" + "no=" + no + '}';
    }
}

单向环形链表实现:

public class CircleSingleLinkedList {
    private BoyNode first; // 单向环形链表的第一个节点,不会移动

    /**
     * 构建一个单向的环形链表思路
     * 1. 先创建第一个节点, 让 first 指向该节点,并形成环形
     * 2. 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可.
     *
     * @param boyNum
     */
    public void addBoyNodes(int boyNum) {
        BoyNode curBoy = null;
        for (int i = 1; i <= boyNum; i++) {
            BoyNode boyNode = new BoyNode(i);
            if (i == 1) {
                first = boyNode;
                first.setNext(first);
                curBoy = boyNode;
            } else {
                curBoy.setNext(boyNode);
                boyNode.setNext(first);
                curBoy = boyNode;
            }
        }
    }

    /**
     * 遍历环形链表
     * 1. 先让一个辅助指针(变量) curBoy,指向first节点
     * 2. 然后通过一个while循环遍历 该环形链表即可 curBoy.next  == first 结束
     */
    public void listBoyNodes() {
        if (first == null) {
            System.out.println("链表为空");
            return;
        }
        BoyNode curBoy = first;
        while (true) {
            System.out.println(curBoy);
            if (curBoy.getNext() == first) {
                break;
            }
            curBoy = curBoy.getNext();
        }
    }

    /**
     * 从第k个开始,每数m个,出列一个
     * <p>
     * 1.  需求创建一个辅助指针(变量) helper , 事先应该指向环形链表的最后这个节点.
     * 补充: 小孩报数前,先让 first 和  helper 移动 k - 1次
     * 2.  当小孩报数时,让first 和 helper 指针同时 的移动  m  - 1 次
     * 3.  这时就可以将first 指向的小孩节点 出圈
     * first = first .next
     * helper.next = first
     * 原来first 指向的节点就没有任何引用,就会被回收
     *
     * @param startNo  k
     * @param countNum m
     */
    public void calcJosephuOrder(int startNo, int countNum) {
        BoyNode beforeCurNode = first, curNode = first;

        // 将 beforeCurNode 执行 first 前一个节点
        while (true) {
            if (beforeCurNode.getNext() == first) {
                break;
            }
            beforeCurNode = beforeCurNode.getNext();
        }

        // 移动 startNo 个位置,然后开始出圈
        for (int i = 0; i < startNo - 1; i++) {
            beforeCurNode = beforeCurNode.getNext();
            curNode = curNode.getNext();
        }

        // 每移动 countNum,出圈一个
        // 当最后剩下一个节点时,停止循环
        while (true) {
            if (beforeCurNode == curNode) {
                break;
            }
            // 移动 countNum - 1 次,出圈一个
            for (int i = 0; i < countNum - 1; i++) {
                beforeCurNode = beforeCurNode.getNext();
                curNode = curNode.getNext();
            }
            System.out.printf("出圈的是 %d \n", curNode.getNo());
            curNode = curNode.getNext();
            beforeCurNode.setNext(curNode);
        }

        System.out.printf("最后剩下的是 %d \n", curNode.getNo());
    }
}

测试功能:

public class CircleSingleLinkedListTest {
    public static void main(String[] args) {
        CircleSingleLinkedList list = new CircleSingleLinkedList();
        list.addBoyNodes(5);
        list.listBoyNodes();

        list.calcJosephuOrder(1,2);
    }
}
posted @ 2020-09-13 11:07  流星<。)#)))≦  阅读(360)  评论(0编辑  收藏  举报