Java数据结构与算法之链表,你实现单链表,但你实现过双链表吗?链表倒序呢?链表翻转呢?合成呢?约瑟夫问题又是什么问题?这些经典的面试题你真的懂吗?

既然选择了看,一定要看完每句话,我不会讲废话,虽然有点长,但是说得很通俗易懂!而且,从单链表开始,数据结构已经开始有点难,你不要以为简单,理解了就觉得简单,往往越简单的东西越会要你老命,那些java基础什么乱七八糟的语言底层,都是采用数据结构去优化执行效率的

链表

单链表

单链表是由一个个节点连接起来的数据结构,其中包含一个数据域和一个指针域,数据域用来存储当前节点的数据,指针域用来指向下一个节点.
下面是带头节点的单链表(也有不带头结点的)

单链表实现起来很简单,首先我们要明白,我们要保存什么.一个节点包含数据域和指针域,这样我们就可以拟定一个类,类中包含一个数据项和指向下一个节点的指针项:

    public class Node<E>{
        E data;
        Node<E> next;

        public Node(E data,Integer sort,Node<E> next) {
            this.data = data;
            this.next=next;
        }
    }

我们可以思考单链表的特点,因为是一条链子,链子可拆卸,也可重组,所以这些数据不是像数组一样是有序的,如果我们要保存插入顺序,可以在Node类里加上一个排序字段sort.

代码例子

单链表不难理解,如果需要对一个节点进行操作,只需要改变指针的指向即可,下面是单链表一些普通操作示例:

package com.hyb.ds.链表.单向链表;

import java.util.ArrayList;
import java.util.List;

public class SingleLinkedList<E> {

    private final Node<E> head=new Node<>(null,-1,null);

    private static Integer size=0;

    private Integer index(Integer size){
        return size;
    }

    private Node<E> node(E data){
        return new Node<>(data,index(size+1), null);
    }




    //尾插法
    public void append(E data){
        Node<E> newNode = node(data);
        Node<E> headNode=head;
        while (headNode.next != null) {
            headNode = headNode.next;
        }
        headNode.next=newNode;
        size++;
    }

    //头插法
    public void linkedHead(E data){
        Node<E> headNode=head;
        Node<E> node = node(data);
        if (isEmpty()){
            headNode.next=node;
            return;
        }
        Node<E> firstNode = headNode.next;
        headNode.next=node;
        node.next=firstNode;
        size++;

    }

    //指定位置插入
    public void appendForIndex(Integer index,E data){

        if (!isLegal(index))
            return;

        Node<E> head = this.head;
        Node<E> node = node(data);

        for (int i = 1; i <= size; i++) {
            //如果到了最后一个节点
            if (isEmpty()){
                head.next=node;
            }
            if (i==index){
                addLinked(head,head.next,node);
                break;
            }
            head=head.next;
        }
    }

    private boolean isLegal(Integer index) {
        return index>-1&&index<=size;
    }


    private void addLinked(Node<E> head,Node<E> last,Node<E> node){
        head.next=node;
        node.next=last;
        size++;
    }

    //删除一个节点
    public void delNode(Integer sort){
        Node<E> head = this.head;
        if (isEmpty()||(!isLegal(sort)))
            return;
        for (Node<E> h=head.next;h.next!=null;h=h.next){
            //如果当前节点的下一个节点与sort相同,就删除下一个节点
            if (sort.equals(h.next.sort)){
                //将当前节点和下一个节点传入
                removeLinked(h,h.next);
                break;
            }
        }
    }
    //查找某个位置的节点
    public E getValue(Integer sort){
        if (isEmpty()||(!isLegal(sort)))
            return null;
        Node<E> head = this.head;
        for (Node<E> h=head.next;h.next!=null;h=h.next) {
            if (sort.equals(h.sort)){
                return h.data;
            }
        }
        return null;
    }

    //查找倒数第k位的节点
    public E getReversalValue(Integer sort){
        if (isEmpty()||(!isLegal(sort)))
            return null;
        Node<E> head=this.head;
        for (int i = 0; i <= size - sort; i++) {
            head=head.next;
            if ((size-sort+1)==head.sort)
                return head.data;
        }
        return null;
    }

    private void removeLinked(Node<E> h,Node<E> next) {
        //如果下一个节点是最后一个节点
        if (next.next==null){
            //让当前节点为最后一个节点
            h.next=null;
            return;
        }
        //如果下一个节点不是最后一个节点
        h.next=next.next;
        size--;

    }

    public void delNodeByData(E data){
        Node<E> head = this.head;
        if (isEmpty()){
            return;
        }
        for (Node<E> x=head.next;x.next!=null;x=x.next){
            if (data.equals(x.next.data)){
                removeLinked(x,x.next);
                break;
            }
        }
    }


    public List<E> list(){
        if (isEmpty())
            return null;
        Node<E> headNode=head;
        List<E> list=new ArrayList<>();
        while (headNode.next!=null){
            headNode=headNode.next;
            list.add(headNode.data);
        }
        return list;
    }

    public boolean isEmpty(){
        return head.next==null;
    }

    private static class Node<E>{
        E data;
        Integer sort; //插入顺序
        Node<E> next;

        public Node(E data,Integer sort,Node<E> next) {
            this.data = data;
            this.sort=sort;
            this.next=next;
        }
    }
}

class Test{
    public static void main(String[] args) {
        SingleLinkedList<Integer> linkedList = new SingleLinkedList<>();
        for (int i = 0; i < 10; i++) {
            linkedList.append(i);
        }
        List<Integer> list1 = linkedList.list();
        list1.forEach(System.out::println);
        //测试头插法
        linkedList.linkedHead(111);
        System.out.println("=============");
        List<Integer> list2 = linkedList.list();
        list2.forEach(System.out::println);
        //测试指定位置插入
        linkedList.appendForIndex(1,222);
        System.out.println("=============");
        List<Integer> list3 = linkedList.list();
        list3.forEach(System.out::println);
        //测试删除一个节点
        linkedList.delNode(1);
        System.out.println("============");
        List<Integer> list = linkedList.list();
        list.forEach(System.out::println);
        //测试删除一个节点
        linkedList.delNodeByData(3);
        System.out.println("============");
        List<Integer> list4 = linkedList.list();
        list4.forEach(System.out::println);
        //按照插入顺序查找
        Integer value = linkedList.getValue(2);
        System.out.println("============");
        System.out.println(value);
        //查找插入顺序倒数的值
        Integer reversalValue = linkedList.getReversalValue(1);
        System.out.println("============");
        System.out.println(reversalValue);

    }
}

链表翻转

不要被这个问题迷惑了,链表翻转不是很简单的,它并不是要求你单单的将头指针指向最末端的节点节课,它还要求你改变每个节点的指向,也就是说,如果n1.next=n2那么翻转过来的话,就要是n2.next=n1

是不是觉得头指针指向尾部就可以了? no,哪有这么简单,我们还需要求助一个新的指针变量

看完上面的图,我们可以叙述下流程:

  1. 新建一个头结点,等于新建一个空的单链表
  2. 将原链表的数据从头到尾用头插法插入到新的单链表中
  3. 最后将原链表的头节点指向新链表的第一个节点就可以了


下面是一个错误的实例:

//    链表翻转
    public void reverseNode(){
        if (isEmpty()||head.next.next==null)
            return;
        //拿到头节点
        Node<E> head = this.head;
        //新建头结点
        Node<E> newHead = new Node<E>(null,0,null);

        Node<E> next=null;

        while (head.next!=null){
            next=head.next;
            if (newHead.next==null){
                newHead.next=head.next;
                head=next;
                continue;
            }
            Node<E> first = newHead.next;
            newHead.next=head.next;
            head.next=first;
            head=next;
        }

        //让原头结点指向新头结点的下一个,即新链表的第一个节点
        this.head.next=newHead.next;
    }

上面是一个典型的失败案例,节点从头节点开始,这个头节点是没有数据的,所以我们每次使用的时候都是要用head.next,但是值得注意的是,head.next.next在这个过程是会改变的,也就是说,我们要在这个过程中保留当前这个节点的下一个节点,有些人错就错在,以为next这个变量保留的就是当前使用的这个节点的下一个节点,这个想法是没错的,但是错就错在你从head节点开始,当前节点就是head.next,你用next保留的是当前节点,并没有保留当前节点下一个节点.
所以,正确的做法应该是下面这个样子:

//    链表翻转
    public void reverseNode(){
        if (isEmpty()||head.next.next==null)
            return;
        //拿到头节点
        Node<E> head = this.head;
        //新建头结点
        Node<E> newHead = new Node<E>(null,0,null);

        Node<E> next=null;

        while (head.next!=null){
            //这里不用判断为空,需要判断的话,你可以试试,最后的代码都是一样的

            next=head.next.next;


            Node<E> p1 = head.next;

            p1.next=newHead.next;

            newHead.next=p1;


            head.next=next;
        }

        //让原头结点指向新头结点的下一个,即新链表的第一个节点
        this.head.next=newHead.next;
    }

这样子从头节点开始容易乱,我们直接从第一个节点开始就可以了:

  public void reverse(){
        if (isEmpty()||head.next.next==null)
            return;
        //辅助执行,指向第一个节点
        Node<E> p=head.next;
        //定义中间变量next,防止head.next被篡改,得到p的下一个节点
        Node<E> next=null;
        //定义一个新的链表
        Node<E> newNode=new Node<>(null,0,null);

        while (p!=null){
            //先保存p的下一个节点,防止p中间被修改无法获取p.next
            next=p.next;
            //断开p与后面的所有节点
            p.next=newNode.next;
            //让新的头节点指向p
            newNode.next=p;
            //p向后移
            p=next;
        }
        //让老头节点指向新的头节点后的第一个节点
        head.next=newNode.next;

    }

主要的思路就是: 一定要注意,因为中间涉及到节点的迁移,当前正在使用的节点的下一个节点是会变的,如果不信你可以去debug一下看看.

反向打印单链表

  1. 可以先翻转链表,然后遍历就可以了,但是这样会破坏单链表的结构
  2. 可以压入栈中,然后利用栈的特性弹出来,就是反过来的打印结果.
  3. 什么是栈? 俗话说就是一个坑,坑底部是封住的,顶部是开口的,每次放入一个东西,坑就会多出一项,直到坑满,每次从坑拿出一个东西,都得先拿上面的,也就是后面放进去的东西可以先拿,所以坑有先进后出的特性.
  4. 怎样去实现栈? 可以用一个数组,索引往0这边靠的就是栈底方向,索引越大的就是栈顶方向,每次增加一个值,索引增1,直到数组吗满,每次拿出一个值,都必须得从索引大的开始拿,索引减1.
  5. 所以我们可以利用栈的特性,很好的将某些东西翻转过来,比如单链表,我们可以先正序遍历单链表,然后顺序地将一个个节点放入到栈中,之后利用栈的特性,拿出来就得从后面拿,这个时候拿到值刚好是单链表从后面遍历的值.
    //单链表反向打印
    public List<E> reverseList(){
        //拿到第一个节点
        Node<E> p = head.next;
        //创建一个栈
        Stack<E> stack = new Stack<>();
        //遍历单链表
        for (Node<E> p1=p;p1!=null;p1=p1.next){
            //入栈
            stack.push(p1.data);
        }
        return new ArrayList<>(stack);
    }

合成两个升序的单链表,合成后依然升序

  1. 我们肯定得先缔造出一个空链表.
  2. 一起遍历两个链表,比较节点之间的大小,因为是升序,所以我们就将节点值小的那个节点放入p3链表中.这个时候,刚放入的节点所在链表的指针往后移,没有节点放入的链表不要移动.下一次比较,继续比较指针所指向节点的值.比如: p1指向的第一个节点为1小于p2指向的第一个节点3,所以将1放入p3的第一个节点中,这个时候,p1后移指向了4,这个时候因为3没有放入p3中,所以p2不用移动,还是指向3,这个时候继续比较4>3,所以3放入p3中,p2后移指向了6,p1此刻不用移动,依旧指向4,继续比较,4<6,4放入p3,p1后移指向5,p2不动指向6,继续比较,5<6,5放入p3,p1.next为空,退出循环,p2指向不动,但是6>5,所以p2指向的6后面的链表都大于5,所以直接将p2此刻后面的所有节点链接到p3后面,得到升序后的排列.
  3. 我们可以看到,因为链表升序的特性,我们p2的每个节点,最多比3次,就是在p1的所有节点都小于p2的节点的时候,因为升序的特性,我们可以直接将p2链接到p3尾部,p2的后面节点就不用比较了.所以如果我们每个节点的比较次数是3,那么总比较次数就是3,如果每个节点的比较次数是2,总比较次数也只能是6,如果每个节点比较是1,那就更少了,所以你会发现,这里的比较次数都不会大于6,也就是m+n,这就是你为什么总在网上看到说时间复杂度最大是o(m+n)的原因.
  4. 至于空间复杂度,如果你使用的while循环,也就是迭代,是o(1),因为你自始至终都只是声明一次变量,分配也就1.而如果你使用递归就不一样,每次都会进入函数,也就是都会声明新的变量,那么各自的变量最多为声明为m次和n次,所以空间复杂度最大依旧是o(m+n).
   //两个升序链表合成一个升序链表
    public SingleLinkedList<E> mergeNode(SingleLinkedList<E> e1,SingleLinkedList<E> e2){
        Node<E> p1 = e1.head.next;
        Node<E> p2 = e2.head.next;
        SingleLinkedList<E> newLinkedList = new SingleLinkedList<>();
        Node<E> p3 = newLinkedList.head;
        //如果p1为空,将p2链接到p3尾部
        if (p1==null&&p2!=null){
            p3.next=p2;
            return newLinkedList;

        //反之将p1链接到p3尾部
        }else if(p1!=null&&p2==null){
            p3.next=p1;
            return newLinkedList;
        }

        while (p1!=null&&p2!=null){
            E d1 = p1.data;
            E d2 = p2.data;
            if (d1 instanceof Integer && d2 instanceof Integer){
                //如果p1所在节点的值小于p2所在节点的值
                if ((Integer)d1<(Integer) d2){
                    //将p1节点值放入p3
                    p3.next=p1;
                    //p1后移
                    p1=p1.next;
                //相反情况
                }else if ((Integer)d1>(Integer) d2){
                    p3.next=p2;
                    p2=p2.next;
                //如果两个值相等,两个指针都向后移动
                }else {
                    p3.next=p1;
                    p1=p1.next;
                    p2=p2.next;
                }
                //p3后移
                p3=p3.next;
            }else
                throw new RuntimeException("不支持该类型比较");

            //如果某个链表先遍历完,将剩下的没遍历的链表直接套在p3后
            if (p1==null||p1.next==null){
                p3.next=p2;
            }
            if (p2==null||p2.next==null){
                p3.next=p1;
            }
        }
        return newLinkedList;
    }

测试(测试一定要是两个升序的链表):

        SingleLinkedList<Integer> p1 = new SingleLinkedList<>();
        SingleLinkedList<Integer> p2 = new SingleLinkedList<>();
        p1.append(1);
        p1.append(77);
        p1.append(9900);
        p2.append(30);
        p2.append(633);
        p2.append(899);

        List<Integer> list7 = p1.list();
        System.out.print("p1:");
        list7.forEach(System.out::print);
        System.out.println();
        System.out.print("p2:");
        List<Integer> list8 = p2.list();
        list8.forEach(System.out::print);
        System.out.println();
        System.out.println("合并后后会改变p1,p2指向:");

        SingleLinkedList<Integer> p3= new SingleLinkedList<>();
        SingleLinkedList<Integer> integerSingleLinkedList = p3.mergeNode(p1, p2);

        List<Integer> list6 = integerSingleLinkedList.list();
        list6.forEach(System.out::println);

双向链表

双向链表就是单链表的升级,其提供一个节点指向前一个节点的指针和指向后一个节点的指针.
就好像正反两条内容一样的单链表重合在了一起,其最大的好处是,在任何一个节点都能知晓其前后所有节点的数据.
双向链表实现起来也很简单,只要弄清楚单链表的操作是如何移动指针的就可以了.

package com.hyb.ds.链表.双向链表;

import java.util.ArrayList;
import java.util.List;

public class DoubleLinkedList<E> {

    private final Node<E> head=new Node<E>(null,0);

    private static Integer size=0;

    private Node<E> node(E data, Integer sort){
        return new Node<E>(data,sort, null, null);
    }

    private Node<E> node(E data, Integer sort,Node<E> pre,Node<E> next){
        return new Node<E>(data,sort, pre, next);
    }

    private boolean isEmpty(Node<E> node){
        return node==null;
    }

    //尾插法
    public void append(E data){
        Node<E> head = this.head;
        Node<E> node = node(data,size+1);
        if (isEmpty(head.next)){
            head.next=node;
            node.pre=head;
            size++;
            return;
        }
        while (head.next!=null)
            head=head.next;
        head.next=node;
        node.pre=head;
        size++;
    }
    //头插法
    public void appendHead(E data){
        Node<E> head = this.head;
        Node<E> node = node(data,size+1);


        Node<E> first = head.next;

        node.next=first;
        if (first!=null){
            first.pre=node;
        }

        head.next=node;
        node.pre=head;

        size++;
    }

    //中间插入一个节点
    public void appendMiddle(Integer index,E data){
        if (!isIndex(index))
            return;

        Node<E> head = this.head;
        Node<E> node = node(data, size + 1);
        if (isEmpty(head.next)){
            head.next=node;
            node.pre=head;
            return;
        }
        for (int i = 1; i <= size; i++) {
            head=head.next;
            if (i==index){
                head.pre.next=node;
                node.pre=head.pre;
                head.pre=node;
                node.next=head;
                break;
            }
        }
        size++;
    }
    //删除一个节点
    public void delNode(Integer index){
        if (!isIndex(index)||isEmpty(head.next))
            return;
        Node<E> head = this.head;
        for (int i = 1; i <= size; i++) {
            head=head.next;
            if (i==index){
                head.pre.next=head.next;
                head.next.pre=head.pre;
                break;
            }
        }
        size--;
    }


    private boolean isIndex(Integer index){
        return index>0&&index<=size;
    }

    //遍历数据
    public List<E> list(){
        Node<E> head = this.head.next;
        List<E> list=new ArrayList<>();
        for (Node<E> node=head;node!=null;node=node.next){
            list.add(node.data);
        }
        return list;
    }



    private static class Node<E>{
        E data;
        Integer sort;
        Node<E> pre;
        Node<E> next;

        public Node(E data, Integer sort) {
            this.data = data;
            this.sort = sort;
        }

        public Node(E data, Integer sort, Node<E> pre, Node<E> next) {
            this.data = data;
            this.sort = sort;
            this.pre = pre;
            this.next = next;
        }
    }
}
class Test{
    public static void main(String[] args) {
        DoubleLinkedList<Integer> doubleLinkedList = new DoubleLinkedList<>();
        for (int i = 0; i < 10; i++) {
            doubleLinkedList.append(i);
        }
        System.out.println("测试尾插法:");
        List<Integer> list = doubleLinkedList.list();
        list.forEach(System.out::println);
        System.out.println("测试头插法:");
        DoubleLinkedList<Integer> integerDoubleLinkedList = new DoubleLinkedList<>();
        for (int i = 0; i < 10; i++) {
            integerDoubleLinkedList.appendHead(i);
        }
        List<Integer> list1 = integerDoubleLinkedList.list();
        list1.forEach(System.out::println);
        System.out.println("测试中间插入:");
        integerDoubleLinkedList.appendMiddle(1,1111);
        List<Integer> list2 = integerDoubleLinkedList.list();
        list2.forEach(System.out::println);
        System.out.println("测试删除一个节点:");
        integerDoubleLinkedList.delNode(1);
        List<Integer> list3 = integerDoubleLinkedList.list();
        list3.forEach(System.out::println);
    }
}

单向环形链表

代码例子

环形链表如环形队列一般,不过链表是链子结构,对增删更加友好.
实现起来也很简单,就如之前的单向链表实现一样,只不过最后让尾结点指向头节点就好了.

package com.hyb.ds.链表.单向环形链表;

import java.util.ArrayList;
import java.util.List;

public class SingleCircleLinkedList<E> {

    private final Node<E> head=new Node<>(null,0,null);

    //创建环形链表
    public void createLinkedList(E ...value){

        Node<E> head = this.head;
        Node<E> last=null;
        for (int i = 0; i < value.length; i++) {
            if (i==0){
                head.data=value[0];
                last=head;
                continue;
            }
            Node<E> eNode = new Node<>(value[i], i, null);
            head.next= eNode;
            head=head.next;
            if (i== value.length-1){
                last=eNode;
            }
        }

        if (last != null) {
            last.next=this.head;
        }

    }
    //遍历环形链表
    public List<E> list(){
        Node<E> head = this.head;
        List<E> list=new ArrayList<>();
//        for循环头节点遍历不出来,我也不知道为啥
        list.add(head.data);
        for (Node<E> node=head.next;node!=head;node=node.next){
            list.add(node.data);
            System.out.println("node.data="+node.data);
            System.out.println("node.next.data="+node.next.data);
            System.out.println("head.data="+head.data);
        }
//        while (true){
//            list.add(head.data);
//            if (head.next==this.head)
//                break;
//            head=head.next;
//        }
        return list;
    }

    //约瑟夫问题

    private static class Node<E>{
        E data;
        int sort;
        Node<E> next;

        public Node(E data, Integer sort, Node<E> next) {
            this.data = data;
            this.sort = sort;
            this.next = next;
        }
    }
}

class Test{
    public static void main(String[] args) {
        SingleCircleLinkedList<Integer> circleLinkedList = new SingleCircleLinkedList<>();
        circleLinkedList.createLinkedList(3,5,7,8,1,2);
        List<Integer> list = circleLinkedList.list();
        list.forEach(System.out::println);
    }
}

约瑟夫问题

该问题是单链表实现中的经典问题.
问: 设编号是1到n个人围成环形,约定从编号k(1<=k<=n)开始报数,数到m那个人开始出队,出列它的下一位又开始从1报数,以此类推直到所有人出列,输出出列的序列号.
分析:

  1. 因为涉及到出列问题,有可能在中间出,不利于用数组去解决问题,可以使用单链表,只要出队,改变指针指向就可以了.
  2. 从问题分析,我们得利用一个指针读取到当数到m的时候,读取到了哪个,同时还需要将其前一个人的指向当前的后一个人,因为是单链表,我们用当前指针是很难知道其前一个人的,所以还需要一个指针,永远指向当前的后一个.


如果要断开当前节点的前一个节点,只需要让当前节点的后一个节点指向当前节点的前一个节点,然后让当前节点向前移动即可(看图虚线).但你得注意的是,你得先移动到要断开的那个节点先.
从上面的图我们如果写代码,必须解决指针如果找到的问题,因为我们规定k是未知的,也就是指针从哪个位置开始指向是未知的,找到p1容易,但是也得找到p2,p2永远在p1后面,所以是k-1位,但你要考虑一种极限情况,就是k=1的时候,p2是在第m位的.

    //约瑟夫问题 有n个人,从k开始数起,数到m断开节点,下一个人从1开始数,规则一样
    public int[] josePhu(int n,int k,int m){
        if (head.data==null || k>n || m>n){
            return null;
        }
        Node<E> p1 = this.head;
        Node<E> p2=this.head;
        int[] count=new int[n];
        int cnt=0;
        if (k==1){
            for (int i=1;i<n;i++)
                p2=p2.next;
        }else {
            for (int i=1;i<k;i++){
                p1=p1.next;
                //p2需要少循环一次
                if (i==k-1)
                    break;
                p2=p2.next;
            }
        }
        while (n != 0) {
            //从当前节点开始数,数到m,将两个指针移动到对的位置
            for (int i = 0; i < m-1; i++) {
                p1=p1.next;
                p2=p2.next;
            }
            //先拿当前节点的数据再移动
            E data = p1.data;
            p1 = p1.next;
            p2.next = p1;
            if (data instanceof Integer) {
                count[cnt++] = (Integer) data;
                n--;
            }
        }
        return count;

    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值