Loading

002-链表

一、单链表

1.1 链表(Linked List)介绍

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

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

🔶 链表(带头结点) 逻辑结构示意图如下:

1.2 单链表的应用实例

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

  • 第一种方法在添加英雄时,直接添加到链表的尾部。
  • 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)。
单链表代码实现

package com.atguigu.linkedlist;

public class SingleLinkedListDemo {

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, "林冲", "豹子头");

    SingleLinkedList singleLinkedList = new SingleLinkedList();
    singleLinkedList.addByOrder(hero3);
    singleLinkedList.addByOrder(hero2);
    singleLinkedList.addByOrder(hero4);
    singleLinkedList.addByOrder(hero1);

    singleLinkedList.list();

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

    HeroNode newHero2 = new HeroNode(2, "小卢", "玉麒麟~~");
    singleLinkedList.update(newHero2);

    singleLinkedList.list();

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

    singleLinkedList.delete(1);
    singleLinkedList.delete(3);
    singleLinkedList.delete(2);
    singleLinkedList.delete(4);

    singleLinkedList.list();
}

}

// 定义链表
class SingleLinkedList{
// 初始化头结点,不存放任何数据
private HeroNode head = new HeroNode(0,"","");

//返回头节点


public HeroNode getHead() {
    return head;
}


// 向单链表结尾添加节点
public void add(HeroNode heroNode){
    HeroNode t = head;
    while(t.next!=null){
        t = t.next;
    }

    t.next = heroNode;
}

// 显示/遍历 链表
public void list(){
    if(head.next==null){
        System.out.println("链表为空");
        return;
    }

    HeroNode t = head.next;
    while(t!=null){
        System.out.println(t);
        t = t.next;
    }
}


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

    HeroNode t = head;
    while(t.next!=null){
        if(t.next.no == heroNode.no){
            System.out.println("添加失败,已经存在");
            return;
        }else if(t.next.no < heroNode.no){
            t = t.next;
        }else{     // 找到位置
            heroNode.next = t.next;
            break;
        }
    }
    // heroNode.next = t.next; 语句放在这里也可以!
    t.next = heroNode;
    return;
}

// 修改节点信息,根据no编号来修改,no编号不可以修改
public void update(HeroNode heroNode){
    /*if(head.next == null){
        System.out.println("链表为空");
        return;
    }*/

    HeroNode t = head.next;
    while(t!=null){
        if(t.no == heroNode.no){
            t.name = heroNode.name;
            t.nickname = heroNode.nickname;
            return;
        }
        t = t.next;
    }

    System.out.println("没有找到该节点");
}

public boolean delete(int no){
    /*if(head.next == null){
        System.out.println("链表为空");
        return false;
    }*/

    HeroNode t = head;
    while(t.next != null){
        if(t.next.no == no){
            t.next = t.next.next;   //被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收
            return true;
        }
        t = t.next;
    }

    return false;
}

}

// 定义链表节点
class HeroNode{

// 为什么用public? 因为private在类外不能被访问。需要Getter/Setter才能获取/修改。
public int no;
public String name;
public String nickname;
public HeroNode next;


public HeroNode(int no, String name, String nickname){
    this.no = no;
    this.name = name;
    this.nickname = nickname;
    next = null;
}

@Override
public String toString(){
    return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
}

}

二、单链表面试题

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

显示代码

//获取单链表有效节点的个数
public static int GetNum(HeroNode head){
    HeroNode t = head.next;
    int res = 0;
    while(t!=null){
        res++;
        t = t.next;
    }

    return res;
}

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

【剑指offer 22】 答案

3. 单链表的反转

【leetcode 206】

4. 从尾到头打印单链表

【方式1:反向遍历 。 方式2:Stack栈】

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

【leetcode 21】

三、双向链表

3.1 单向链表的缺点分析:

  • 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
  • 单向链表不能自我删除,需要靠辅助节点(待删除节点的前一个节点),而双向链表,则可以自我删除。

3.2 双向链表代码实现:

双向链表代码实现

package com.atguigu.linkedlist;

public class DoubleLinkedListDemo {

public static void main(String[] args) {

    System.out.println("双向链表的测试:");

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

    DoubleLinkedList d = new DoubleLinkedList();
    d.addByOrder(hero4);
    d.addByOrder(hero2);
    d.addByOrder(hero3);
    d.addByOrder(hero1);

    d.list();

    //修改
    System.out.println("双向链表的update测试:");
    HeroNode2 hero5 = new HeroNode2(4, "公孙胜", "入云龙");
    d.update(hero5);
    d.list();


    //删除
    d.delete(3);
    System.out.println("双向链表的delete测试:");
    d.list();

}

}

class DoubleLinkedList{

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

//返回头节点  (单/双链表相同)
public HeroNode2 getHead() {
    return head;
}

// 显示/遍历 链表  (单/双链表相同)
public void list(){
    if(head.next==null){
        System.out.println("链表为空");
        return;
    }

    HeroNode2 t = head.next;
    while(t!=null){
        System.out.println(t);
        t = t.next;
    }
}

// 向双链表结尾添加节点
public void add(HeroNode2 heroNode){
    HeroNode2 t = head;
    while(t.next!=null){
        t = t.next;
    }

    t.next = heroNode;
    heroNode.pre = t;
}

// 按排名顺序添加链表
public void addByOrder(HeroNode2 heroNode){

    HeroNode2 t = head;
    while(t.next!=null){
        if(t.next.no == heroNode.no){
            System.out.println("添加失败,已经存在");
            return;
        }else if(t.next.no < heroNode.no){
            t = t.next;
        }else{     // 找到位置
            break;
        }
    }
    heroNode.next = t.next;
    t.next = heroNode;
    heroNode.pre = t;
    if(heroNode.next != null){
        heroNode.next.pre = heroNode;
    }
}


// 修改节点信息,根据no编号来修改,no编号不可以修改
public void update(HeroNode2 heroNode){

    HeroNode2 t = head.next;
    while(t!=null){
        if(t.no == heroNode.no){
            t.name = heroNode.name;
            t.nickname = heroNode.nickname;
            return;
        }
        t = t.next;
    }

    System.out.println("没有找到该节点");
}


public boolean delete(int no){
    /*if(head.next == null){
        System.out.println("链表为空");
        return false;
    }*/

    HeroNode2 t = head.next;
    while(t != null){
        if(t.no == no){
            t.pre.next = t.next;
            if(t.next!=null) {   //要判断,t有可能是最后一个节点,next域为空
                t.next.pre = t.pre;
            }
            return true;
        }
        t = t.next;
    }

    return false;
}

}

class HeroNode2{

// 为什么用public? 因为private在类外不能被访问。需要Getter/Setter才能获取/修改。
public int no;
public String name;
public String nickname;
public HeroNode2 next;   //默认为空,不用修改
public HeroNode2 pre;


public HeroNode2(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 + "]";
}

}

四、单向环形链表与约瑟夫问题

4.1 单向环形列表

4.2 Josephu 问题

Josephu问题描述

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

解题思路1

用一个不带头结点的循环链表来处理 Josephu 问题:

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

单向环形链表实现

package com.atguigu.linkedlist;

public class Josephu {

public static void main(String[] args) {

    CircleSingleLinkedList c = new CircleSingleLinkedList();
    c.addNote(125);
    c.show();

    //测试小孩出圈
    c.outCircle(10,20,125);
}

}

// 环形单向链表
class CircleSingleLinkedList{

private Node first = new Node(-1);

// 构建环形链表
public void addNote(int nums){
    if(nums < 1){
        System.out.println("nums不正确!");
        return;
    }

    Node cur = null;   // 无需赋值构造函数,可以直接赋值?

    for(int i=1; i<=nums; i++){
        Node newN = new Node(i);

        if(i==1){
            first = newN;  //first = newN; 与first.setNext(newN)区分
            newN.setNext(first);   //指向自身
            cur = first;
        }else{
            cur.setNext(newN);
            newN.setNext(first);
            cur = newN;
        }


    }

}

public void show(){
    if(first == null){
        System.out.println("链表为空");
        return;
    }

    Node t = first;

    while(t.getNext()!=first){  // 遍历中止条件
        System.out.printf("小孩编号 %d \n", t.getNo());
        t = t.getNext();
    }
    System.out.printf("小孩编号 %d \n", t.getNo());
}


//出圈顺序
/*
* @param startNo  表示从第几个小孩开始数数
* @param countNum 表示数几下
* @param nums 表示最初有多少小孩在圈中
* */
public void outCircle(int startNo, int countNum, int nums){
    //先对数据校验
    if(first == null || startNo < 1 || startNo > nums){
        System.out.println("参数输入有误");
        return;
    }

    // 创建辅助指针
    Node helper = first;
    //需求创建一个辅助指针(变量) helper, 事先应该指向环形链表的最后这个节点(first的前面)
    while(helper.getNext()!=first){
        helper = helper.getNext();
    }


    //first移动到startNo位置,helper对应移动
    for(int i=0; i<startNo-1; i++){
        first = first.getNext();
        helper = helper.getNext();
    }

    //当小孩报数时,让first与helper指针同时移动countNum-1次,然后出圈。
    while(true){
        if(helper == first){ //圈中只剩一个人
            break;
        }

        for(int j=0; j<countNum-1; j++){
            first = first.getNext();
            helper = helper.getNext();
        }

        //此时first指向的是要出圈的小孩
        System.out.printf("小孩 %d 出圈\n", first.getNo());
        //helper.setNext(first.getNext());  
        //first = helper.getNext();
        first = first.getNext();
        helper.setNext(first);
    }

    System.out.printf("最后留在圈中的小孩编号 %d \n", first.getNo());
}

}

// 节点类
class Node{

private int no;
private Node next;

Node(int no){
    this.no = no;
}

public int getNo() {
    return no;
}

public void setNo(int no) {
    this.no = no;
}

public Node getNext() {
    return next;
}

public void setNext(Node next) {
    this.next = next;
}

}

单向链表删除时,需要待删除节点的上一个节点信息,此处helper指针即为此作用,同时用于判断链表只有一个节点情况。

解题思路2

数学推导:

首先对于对于第一轮:

0, 1, 2, ......, M-2, M-1, M, M+1, ......, N-2, N-1 ---------------------------------数组1 (N个人)

剩下的人:

0, 1, 2, ......, M-2, M, M+1, ......, N-2, N-1

对剩下的人的排列换成下面这样:

M, M+1, M+2, ......, N-2, N-1, 0, 1, 2, ......, M-2 --------------------------------数组2 (N-1个人)

很容易发现上面的数组2等价于:

{(x+M)mod N | x = 0, 1, 2, ......, n-3, n-2} ------------------------------------数组3;

很明显如果知道了数组 2 中幸存者编号,那也等于知道了数组 1 中幸存者的编号;

而数组 2 等价于数组 3,所以现在问题变成了从 0 到 n-2 的人中找幸存者;即假设我们从0到 n-2 中找到了幸存者编号为 x,那么根据公式 (x + M)mod N 即可得到数组 2 中的幸存者编号,也即数组1 N 个人中的幸存者。

到此这个问题就是就可以总结为,如果知道了N-1个人中幸存者的编号,则可计算出N个人中幸存者的编号;

N应该大于等于1,而当 N=1 时,只有一个人,那幸存者编号就是 x1=0(从0开始计数);接着可以计算出 N=2 时的幸存者编号x2=(x1+M)%2,然后利用 N=2 可以计算出N=3 ... ...;

数学推导实现

public class JosephuMath {
public static void main(String[] args) {
    int n = 5, m = 2;
    System.out.println(josephu(n, m));
}

public static int josephu(int n, int m) {
    int ret = 0, i;

    for (i = 2; i <= n; i++) {
        ret = (ret + m) % i;
    }

    return ret;
}

}

资料整理于 哔哩哔哩 尚硅谷韩顺平Java数据结构与算法

posted @ 2020-06-25 13:43  喵喵巫  阅读(207)  评论(0编辑  收藏  举报