数据结构和算法(三)线性表——链表

链表由若干个“节点”组成,每个节点含有数据域和指针域

有一个指针域,就是单向链表;两个指针域,可以组成双向链表

链表有链头和链尾,链头数据域必须为null,链尾指针域必须为null

 单链表:

单链表节点包含一个数据域和一个指针域

链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点

链表插入和删除只需要修改指节点对应的针域指针指向即可!

数据结构和算法(三)线性表——链表_第1张图片

单链表API设计:

数据结构和算法(三)线性表——链表_第2张图片

其中包含实现单链表的递归反转(面试考点):

需求:
原链表中数据为: 1->2->3>4
反转后链表中数据为: 4->3->2->1
 
数据结构和算法(三)线性表——链表_第3张图片
 
package cn.itcast.algorithm.linear;

import java.util.Iterator;

public class LinkList implements Iterable{
    //记录头结点
    private Node head;
    //记录链表的长度
    private int N;

    //结点类
    private class Node {
        //存储数据
        T item;
        //下一个结点
        Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }

    public LinkList() {
        //初始化头结点、
        this.head = new Node(null,null);
        //初始化元素个数
        this.N=0;
    }

    //清空链表
    public void clear() {
        head.next=null;
        this.N=0;
    }

    //获取链表的长度
    public int length() {
        return N;
    }

    //判断链表是否为空
    public boolean isEmpty() {
        return N==0;
    }

    //获取指定位置i出的元素
    public T get(int i) {

        //通过循环,从头结点开始往后找,依次找i次,就可以找到对应的元素
        Node n = head.next;
        for(int index=0;index iterator() {
        return new LIterator();
    }

    private class LIterator implements Iterator{
        private Node n;
        public LIterator(){
            this.n=head;
        }

        @Override
        public boolean hasNext() {
            return n.next!=null;
        }

        @Override
        public Object next() {
            n = n.next;
            return n.item;
        }
    }

    //用来反转整个链表
    public void reverse(){

        //判断当前链表是否为空链表,如果是空链表,则结束运行,如果不是,则调用重载的reverse方法完成反转
        if (isEmpty()){
            return;
        }

        reverse(head.next);
    }

    //反转指定的结点curr,并把反转后的结点返回
    public Node reverse(Node curr){
        if (curr.next==null){
            head.next=curr;
            return curr;
        }
        //递归的反转当前结点curr的下一个结点;返回值就是链表反转后,当前结点的上一个结点
        Node pre = reverse(curr.next);
        //让返回的结点的下一个结点变为当前结点curr;
        pre.next=curr;
        //把当前结点的下一个结点变为null
        curr.next=null;
        return curr;
    }


}

双向链表

每个结点都由一个数据域两个指针域组成

其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点

链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null

 

双向链表API设计

数据结构和算法(三)线性表——链表_第4张图片

package cn.itcast.algorithm.linear;

import java.util.Iterator;

public class TowWayLinkList implements Iterable {
    //首结点
    private Node head;
    //最后一个结点
    private Node last;

    //链表的长度
    private int N;



    //结点类
    private class Node{
        public Node(T item, Node pre, Node next) {
            this.item = item;
            this.pre = pre;
            this.next = next;
        }

        //存储数据
        public T item;
        //指向上一个结点
        public Node pre;
        //指向下一个结点
        public Node next;
    }

    public TowWayLinkList() {
       //初始化头结点和尾结点
        this.head = new Node(null,null,null);
        this.last=null;
        //初始化元素个数
        this.N=0;
    }

    //清空链表
    public void clear(){
        this.head.next=null;
        this.head.pre=null;
        this.head.item=null;
        this.last=null;
        this.N=0;
    }

    //获取链表长度
    public int length(){
        return N;
    }

    //判断链表是否为空
    public boolean isEmpty(){
        return N==0;
    }

    //获取第一个元素
    public T getFirst(){
        if (isEmpty()){
            return null;
        }
        return head.next.item;
    }

    //获取最后一个元素
    public T getLast(){
        if (isEmpty()){
            return null;
        }
        return last.item;
    }

    //插入元素t
    public void insert(T t){

        if (isEmpty()){
            //如果链表为空:

            //创建新的结点
            Node newNode = new Node(t,head, null);
            //让新结点称为尾结点
            last=newNode;
            //让头结点指向尾结点
            head.next=last;
        }else {
            //如果链表不为空
            Node oldLast = last;

            //创建新的结点
            Node newNode = new Node(t, oldLast, null);

            //让当前的尾结点指向新结点
            oldLast.next=newNode;
            //让新结点称为尾结点
            last = newNode;
        }

        //元素个数+1
        N++;

    }

    //向指定位置i处插入元素t
    public void insert(int i,T t){
        //找到i位置的前一个结点
        Node pre = head;
        for(int index=0;index iterator() {
        return new TIterator();
    }

    private class TIterator implements Iterator{
        private Node n;
        public TIterator(){
            this.n=head;
        }
        @Override
        public boolean hasNext() {
            return n.next!=null;
        }

        @Override
        public Object next() {
            n=n.next;
            return n.item;
        }
    }

}

双向链表的体现——LinkedList:

java LinkedList 集合也是使用双向链表实现,并提供了增删改查等相关方法
1.底层是否用双向链表实现?
以添加方法参考:
数据结构和算法(三)线性表——链表_第5张图片
看一下节点内部类:
数据结构和算法(三)线性表——链表_第6张图片
2.结点类是否有三个域?
上面节点内部类截图已经给出答案!
 
 

链表的复杂度分析

get(int i): 每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素 N 的增多,比较的元素越多,时间
复杂度为 O(n)
insert(int i,T t): 每一次插入,需要先找到 i 位置的前一个元素,然后完成插入操作,随着数据元素 N 的增多,查找的
元素越多,时间复杂度为 O(n);
remove(int i): 每一次移除,需要先找到 i 位置的前一个元素,然后完成插入操作,随着数据元素 N 的增多,查找的元
素越多,时间复杂度为 O(n)
 
相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为 链表的物理地址是不连续的,
它不需要预先指定存储空间大小 ,好处就是存储过程中不会涉及到 扩容 等操作,同时它并没有涉及的 元素的交换
 
相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删
操作比较多,建议使用链表。
 

快慢指针设计

需求1——通过快慢指针获取中间值问题:

请完善测试类 Test 中的 getMid 方法,可以找出链表的中间元素值并返回。
利用快慢指针,我们把一个链表看成一个跑道,假设 a 的速度是 b 的两倍,那么当 a 跑完全程后, b 刚好跑一半,以
此来达到找到中间节点的目的。
 

 数据结构和算法(三)线性表——链表_第7张图片

package cn.itcast.algorithm.test;

public class FastSlowTest {

    public static void main(String[] args) throws Exception {
        //创建结点
        Node first = new Node("aa", null);
        Node second = new Node("bb", null);
        Node third = new Node("cc", null);
        Node fourth = new Node("dd", null);
        Node fifth = new Node("ee", null);
        Node six = new Node("ff", null);
        Node seven = new Node("gg", null);

        //完成结点之间的指向
        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;

        //查找中间值
        String mid = getMid(first);
        System.out.println("中间值为:"+mid);
    }

    /**
     * @param first 链表的首结点
     * @return 链表的中间结点的值
     */
    public static String getMid(Node first) {
        //定义两个指针
        Node fast = first;
        Node slow = first;
        //使用两个指针遍历链表,当快指针指向的结点没有下一个结点了,就可以结束了,结束之后,慢指针指向的结点就是中间值
        while(fast!=null &&fast.next!=null){
            //变化fast的值和slow的值
            fast = fast.next.next;
            slow=slow.next;
        }

        return slow.item;
    }

    //结点类
    private static class Node {
        //存储数据
        T item;
        //下一个结点
        Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

需求2——通过快慢指针判断单向链表是否有环问题

请完善测试类 Test 中的 isCircle 方法,返回链表中是否有环。
使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道
中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环。
 
数据结构和算法(三)线性表——链表_第8张图片
快慢指针图解:
数据结构和算法(三)线性表——链表_第9张图片
........
.......
数据结构和算法(三)线性表——链表_第10张图片
package cn.itcast.algorithm.test;

public class CircleListCheckTest {
    public static void main(String[] args) throws Exception {
        //创建结点
        Node first = new Node("aa", null);
        Node second = new Node("bb", null);
        Node third = new Node("cc", null);
        Node fourth = new Node("dd", null);
        Node fifth = new Node("ee", null);
        Node six = new Node("ff", null);
        Node seven = new Node("gg", null);

        //完成结点之间的指向
        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;
        //产生环
        seven.next = third;

        //判断链表是否有环
        boolean circle = isCircle(first);
        System.out.println("first链表中是否有环:"+circle);
    }

    /**
     * 判断链表中是否有环
     * @param first 链表首结点
     * @return ture为有环,false为无环
     */
    public static boolean isCircle(Node first) {
        //定义快慢指针
        Node fast = first;
        Node slow = first;

        //遍历链表,如果快慢指针指向了同一个结点,那么证明有环
        while(fast!=null && fast.next!=null){
            //变换fast和slow
            fast = fast.next.next;
            slow = slow.next;

            if (fast == slow){
                return true;
            }
        }

        return false;
    }

    //结点类
    private static class Node {
        //存储数据
        T item;
        //下一个结点
        Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

 需求3——通过快慢指针找到有环链表环的入口节点问题

请完善 Test 类中的 getEntrance 方法,查找有环链表中环的入口结点。
当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样
为1,则慢指针与“新”指针相遇的地方就是环的入口。 证明这一结论牵涉到数论的知识,这里略,只讲实现。

 数据结构和算法(三)线性表——链表_第11张图片

数据结构和算法(三)线性表——链表_第12张图片

package cn.itcast.algorithm.test;

public class CircleListInTest {
    public static void main(String[] args) throws Exception {
        Node first = new Node("aa", null);
        Node second = new Node("bb", null);
        Node third = new Node("cc", null);
        Node fourth = new Node("dd", null);
        Node fifth = new Node("ee", null);
        Node six = new Node("ff", null);
        Node seven = new Node("gg", null);

        //完成结点之间的指向
        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;
        //产生环
        seven.next = third;

        //查找环的入口结点
        Node entrance = getEntrance(first);
        System.out.println("first链表中环的入口结点元素为:"+entrance.item);
    }

    /**
     * 查找有环链表中环的入口结点
     * @param first 链表首结点
     * @return 环的入口结点
     */
    public static Node getEntrance(Node first) {
        //定义快慢指针
        Node fast = first;
        Node slow = first;
        Node temp = null;

        //遍历链表,先找到环(快慢指针相遇),准备一个临时指针,指向链表的首结点,继续遍历,直到慢指针和临时指针相遇,那么相遇时所指向的结点就是环的入口
        while(fast!=null && fast.next!=null){
            //变换快慢指针
            fast = fast.next.next;
            slow = slow.next;

            //判断快慢指针是否相遇
            if (fast.equals(slow)){
                temp = first;
                continue;
            }

            //让临时结点变换
            if (temp!=null){
                temp = temp.next;
                //判断临时指针是否和慢指针相遇
                if (temp.equals(slow)){
                    break;
                }
            }
        }

        return temp;
    }
    //结点类
    private static class Node {
        //存储数据
        T item;
        //下一个结点
        Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

循环链表

循环链表,顾名思义,链表整体要形成一个圆环状。
 

数据结构和算法(三)线性表——链表_第13张图片

    public static void main(String[] args) throws Exception { 
        //构建结点 
        Node first = new Node(1, null); 
        Node second = new Node(2, null); 
        Node third = new Node(3, null); 
        Node fourth = new Node(4, null); 
        Node fifth = new Node(5, null); 
        Node six = new Node(6, null); 
        Node seven = new Node(7, null);
    
        //构建单链表 
        first.next = second; 
        second.next = third; 
        third.next = fourth; 
        fourth.next = fifth; 
        fifth.next = six; 
        six.next = seven; 
        //构建循环链表,让最后一个结点指向第一个结点 
        seven.next = first; 
    }

约瑟夫环问题

问题描述:
传说有这样一个故事,在罗马人占领乔塔帕特后, 39 个犹太人与约瑟夫及他的朋友躲到一个洞中, 39 个犹太人决
定宁愿死也不要被敌人抓到,于是决定了一个自杀方式, 41 个人排成一个圆圈,第一个人从 1 开始报数,依次往
后,如果有人报数到 3 ,那么这个人就必须自杀,然后再由他的下一个人重新从 1 开始报数,直到所有人都自杀身亡
为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第 16 个与
31 个位置,从而逃过了这场死亡游戏 。
问题转换:
41 个人坐一圈,第一个人编号为 1 ,第二个人编号为 2 ,第 n 个人编号为 n
1. 编号为 1 的人开始从 1 报数,依次向后,报数为 3 的那个人退出圈;
2. 自退出那个人开始的下一个人再次从 1 开始报数,以此类推;
3. 求出最后退出的那个人的编号。
图示:
数据结构和算法(三)线性表——链表_第14张图片
解题思路:
1. 构建含有 41 个结点的单向循环链表,分别存储 1~41 的值,分别代表这 41 个人;
2. 使用计数器 count ,记录当前报数的值;
3. 遍历链表,每循环一次, count++
4. 判断 count 的值,如果是 3 ,则从链表中删除这个结点并打印结点的值,把 count 重置为 0
 
public class Main {

    public static void main(String[] args) throws Exception {
        //1.构建循环链表
        Node first = null;
        //记录前一个结点
        Node pre = null;

        for (int i = 1; i <= 41; i++) {
            //第一个元素
            if (i==1){
                first = new Node(i,null);
                pre = first;
                continue;
            }
            Node node = new Node<>(i,null);
            pre.next = node; pre = node;
            if (i==41){
                //构建循环链表,让最后一个结点指向第一个结点
                pre.next=first;
            }
        }

        //2.使用count,记录当前的报数值
        int count=0;
        //3.遍历链表,每循环一次,count++ 
        Node n = first; 
        Node before = null; 
        while(n!=n.next){ 
            //4.判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0; 
            count++; 
            if (count==3){ 
                //删除当前结点 
                before.next = n.next; 
                System.out.print(n.item+","); 
                count=0; 
                n = n.next; 
            }
            else {
                before=n; 
                n = n.next; 
            } 
        }
        
        /*打印剩余的最后那个人*/ 
        System.out.println(n.item);
    }
}

你可能感兴趣的:(Data,structures,and,algorithms)