数据结构学习(基础)——链表——Day04

2.1 哈希表、有序表、单链表和双链表

HashMap就是Key-Value成对出现;HashSet就是可以只有key没有value,两者区别就是有没有伴随数据。

哈希表是无序组织的key

HashMap:put方法既是添加数据也是更新数据,当put中key在Map中没有就是添加,如果有的话就更新key的value值;同时也可以remove(key),删除key同时删除掉伴随数据。查的话,我们可以用containKey方法查这个key是不是存在同时也可以利用get方法查询key所对应的value。

HashSet:add,remove就是添加和删除,可以看这个元素是否存在与Set;contain方法查询key是否存在

哈希表在使用的时候时间复杂度都是常数级别(增、删、改、查)。

①哈希表的简单介绍

    1>哈希表在使用层面可以理解为一种集合结构。

    2>如果只有key,没有伴随数据value,可以使用HashSet结构

    3>如果既有key,又有伴随数据value,可以使用HashMap结构

    4>有无伴随数据,是HashSet和HashMap唯一区别,底层的实际结构是一回事

    5>使用哈希表增(put)、删(remove)、改(put)和查(get)的操作,可以认为时间复杂度为O(1),但是常数时间比较大

    6>放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小

    7>放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小(占用8byte)

TreeSet:只有key

TreeMap:有key和value

有序表是有序组织的key,哈希表能完成的功能有序表都能完成,同时还能根据key有序来添加功能

put:既是添加,又是更新,而且key是可以进行比较的,比如可以找出所有key中的最大和最小,或者小于等于某个key大于等于某个key。性能比哈希表差一点。增删改查都是O(logN)级别的

②有序表的简单介绍

    1>有序表在使用层面上可以理解为一种集合结构

    2>如果只有key,没有伴随数据value,可以使用TreeSet结构

    3>如果既有key,又有伴随数据value,可以使用TreeMap结构

    4>有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事

    5>有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织

    6>红黑树、AVL树,size-balance-tree和跳表都属于有序表结构,只是底层具体实现不同

    7>放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小

    8>放入有序表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小

    9>不管是什么底层具体实现,只要是有序表,都有一下固定的基本功能和固定的时间复杂度

有序表的固定操作:

    1>void put(K key,V value):将一个(key,value)记录加入到表中,或者将key的记录更新成value

    2>V get(K key):根据指定的key,查询value并返回

    3>void remove(K key):移除key的记录

    4>Boolean containKey(K key):询问是否有关于key的记录

    5>K firstKey():返回所有键值的排序结果中,最左(小)的那个

    6>K lastKey():返回所有键值的排序结果中,最右(大)的那个

    7>K floorKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的前一个

    8>K ceilingKey(K key):如果表中存入过key,返回key;否则返回所有键值的排序结果中,key的后一个

单向链表和双向链表都是,给一个头部head就可以找到后面的节点

一个节点的属性next是下一个节点

问题1:分别实现反转单向链表和双向链表的函数(要求时间复杂度为O(N),额外空间复杂度O(1)

/**
     * 1.反转单项链表
     * 1->2->3 变成1<-2<-3
     */
    public static singleNode<Integer> reverseSingleNode(singleNode head){//传来一个头部
        if(head == null || head.next == null){//如果head为null代表链表为空,返回null,如果head的下一个为null代表该链表就只有一个节点返回该节点
            return head;
        }
        //定义该节点的上一个和下一个节点
        //链表连接节点是通过next,无法回溯,所以我们定义preNode,而如果我们完成1 2节点的反转,就会丢失2 3节点的连接,所以我们要通过nextNode完成连接 用于保存上一个和下一个节点
        singleNode<Integer> preNode = null;
        singleNode<Integer> nextNode = null;

        //当head为null的时候就代表已经反转完最后一个了
        while(head != null){
            nextNode = head.next;//需要将下一个节点的地址保存下来,否则我们将这个链表反转后,我们会找不到下一个链表,也就是我们的head找不到下一个node了
            head.next = preNode;//将当前节点的next指向前一个节点的地址    完成1 2 节点的反转后,2 3 之间就没有next作为连接了,所以还想继续完成反转,我们需要让下一个节点
            //
            preNode = head;//由于你当前节点已经完成了链翻转,而我们依旧要完成下一个链的翻转,所以我们要让当前节点成为下一个节点的前一个(虽然已经断开连接)
            head = nextNode;//我们需要将head移动到下一个节点的地址
        }
        return preNode;//这就是源链表的最后一个数也就是翻转后链表的头节点
    }

    /**
     * 2.反转双向链表
     * @param head
     * @return
     */
    public static doubleNode<Integer> reverseDoubleNode(doubleNode head){
        if(head == null || head.next == null){//如果head为null代表链表为空,返回null,如果head的下一个为null代表该链表就只有一个节点返回该节点
            return head;
        }
        doubleNode<Integer> nextNode = null;//定义下一个节点因为我们完成该节点和上一个节点的连接反转后,无法通过next或者是pre找到下一个节点,所以我们先把下一个节点保存下来方便寻找
        doubleNode<Integer> temp = null;//定义当前节点,用于最后返回
        //当head为null的时候就代表已经反转完最后一个了
        while (head != null){
            nextNode = head.next;//将下一个节点先保存下来,避免找不到
            head.next = head.pre;//将当前节点的next指针换到前一个节点,也就是当前节点的pre
            head.pre = nextNode;//该节点的pre指针指到将保存起来的节点(原链表中的下一个节点)
            //完成next和pre的变换后,我们需要换head
            //需要一个变量保存当前值,用于返回
            temp = head;//为什么要这个?     因为要保存当前节点用于最后的返回,节点后移继续反转
            //将head移动到下一个节点,我们之前保存过那个地址
            head = nextNode;
        }
        return temp;
    }

问题2:给定两个有序链表的头指针head1和head2,打印两个链表的公共部分(要求时间复杂度为O(n),额外空间复杂度O(1))

有点类似于外部排序的过程

/**
     * 3.给定两个有序链表的头指针head1和head2,打印两个链表 的公共部分(要求时间复杂度为O(n),额外空间复杂度O(1))
     * 主体思想:
     * 两个链表定义两个指针分别指向链表头部,判断两个指针所指向的节点的value是不是相同,如果相同一起向下移动,如果第一个小于第二个的话第一个下移第二个不动,反之亦然。
     */
    public static void xiangtongLianBiao(singleNode s1,singleNode s2){//传参的时候可以传入两个链表的头部节点
        //定义两个指针表明链表的当前位置
        singleNode p1 = s1;
        singleNode p2 = s2;

        while (p1 != null && p2 != null) {//因为我们p1表示的是当前指针所指的节点,如果有一方为null了,就代表有一方链表走完了,又因为有序所以就不需要比较了
            //判断两个指针当前所指节点的value大小,如果相同一起往下移动,如果不同谁小谁下移动大的不动
            if (p1.value == p2.value) {
                p1 = p1.next;
                p2 = p1.next;
                System.out.println(p1.value);//输出相同内容
            } else if ((int)(p1.value)> (int)(p2.value)) {//如果p1.value是int型可以直接比较,而如果是Integer的话要转为int再比较
                p2 = p2.next;
            } else if ((int)(p1.value)< (int)(p2.value)) {
                p1 = p1.next;
            }
        }
    }

面试时链表解题的方法论:

1.对于笔试,不用太在乎空间复杂度,一切为了时间复杂度

2.对于面试,时间复杂度依然放在第一位,但是一定要找到空间最省的方法

重要技巧:

1.额外数据结构记录(哈希表等)

2.快慢指针

问题3:判断一个链表是否为回文链表(要求时间复杂度O(n),空间复杂度O(1))

方法一:将节点从头到位压入栈中,再从栈中出栈依次跟原链表进行比对(栈是先入后出,后入先出)

/**
     * 回文链表
     */
    /**
     * 方法一:使用额外数据结构——栈,将链表每个元素压栈,链表遍历完,重新遍历,每遍历一个元素,出栈一个元素,并进行对比。-----> 额外空间复杂度O(n)
     */
    //笔试这么做就行
    //因为栈是先进后出的,所以出栈的第一个数也就是原链表的最后一个数再去跟原链表第一个数进行比对
    public static boolean huiwen01(singleNode head){//传入这个链表的头节点
        if(head == null || head.next == null){
            return true;
        }
        Stack<singleNode> stack = new Stack();
        singleNode temp = head;//定义当前节点,往下遍历
        while (temp != null){//将节点放入栈中,当为空的时候停止
            stack.push(temp);
            temp = temp.next;
        }//此时全部节点入栈
        //栈是先进后出,所以弹出来的时候,第一个弹出来的就是最后一个入栈的
        //利用出栈和原链表之间进行比对,当出现不同的时候返回false否则为true
        while (!stack.isEmpty()){//当栈不为空时取数,相当于加一层保障
            //由于我们自己定义的变量temp此时已经在原链表的最后一个位置了,所以我们需要用参数head
            if(head.value != stack.pop().value){//当两数不同时代表不是回文链表
                return false;
            }
            head = head.next;//如果相同的话,head指针后移
        }
        return true;
    }

 方法二:利用快慢指针找到栈的中点,将后半段压入栈中,之后再出栈进行比对

快慢指针:定义快指针和慢指针,快指针每次走两步,慢指针每次走一步,当快指针走完,慢指针正好在中点(奇数个节点)或者是在中间两个数中前面一个(偶数个节点),那么慢指针的下一个就是后半段的第一个节点

/**
     * 方法二:只将链表的一半压入栈,通过快慢指针找到中间位置,并把后半部分压栈,然后和链表前半部分比较 ---> 额外空间复杂度O(n/2)
     */
    /**
     * 必须写熟
     * 链表长度长和短都要对
     * 快慢指针:快指针每次走两步,慢指针每次走一步,快指针走完慢指针就走到中间,这样快慢指针之间的就是链表后半段
     * 根据实际情况去定制,如果链表内有奇数个,那么当快指针走完的时候慢指针应该是最中间的数
     * 如果链表内有偶数个,那么当快指针走完的时候慢指针可能需要指在中间前一个或者是中间后一个
     *
     * 或者有可能当快指针走完的时候慢指针走到中点前一个位置(奇数)
     * 快指针走完的时候慢指针走到中点前两个位置(偶数)
     */
    public static boolean huiwen2(singleNode head){
        if(head == null || head.next == null){
            return true;
        }
        //定义快慢指针,找到重点
        singleNode fast = head;
        singleNode down = head;
        while (fast.next != null && fast.next.next != null){//只有这种情况下,快慢指针才会一起移动,否则就到达了链表最后,不能移动
            //当链表节点为奇数个的时候,快指针在最后一个元素,慢指针正好在中间
            //当链表节点为偶数个的时候,快指针在中间两个数的后一个,慢指针在中间两个数的前一个
            down = down.next;//慢指针每次走一步
            fast = fast.next.next;//快指针每次走两步
        }
        //简单列几个就明白了了
        //要是为奇数个的话,将慢节点后面的都入栈
        //要是为偶数个的话,将慢节点后面的都入栈
        Stack<singleNode> stack = new Stack<>();
        while (down != null){//知道慢指针走完结束
            stack.push(down);//将慢指针所指的节点入栈
            down = down.next;//指针后移
        }
        //此时栈内为后半链的元素
        //依次出栈进行比对
        while (!stack.isEmpty()){
            if(head.value != stack.pop().value){
                return false;
            }
            head = head.next;
        }
        return true;
    }

方法三:利用快慢指针找到后半段,对后半段进行反转再连上,之后定义两个指针从原链表头节点和反转后的第一个节点进行比对,如果都一样就是回文,如果有不一样就不是回文。之后再将反转过后的链表反转回去拼接上返回头节点

/**
     * 能pk掉别人的方法
     * 方法三:通过快慢指针,找到中间位置,反转链表后半部分,进行比较。---> 额外空间复杂度O(1)
     */
    public static boolean huiwen3(singleNode head){
        if(head == null || head.next == null){
            return true;
        }
        //定义快慢指针还有用于遍历链表的指针
        singleNode cur = head;
        singleNode low = head;
        singleNode fast = head;
        //利用快慢指针找到链表重点
        while (fast.next == null || fast.next.next == null){//快指针下一个或者下两个为null代表着链表走完了,找到重点了
            fast = fast.next.next;//快指针每次走两部
            low = low.next.next;//慢指针每次凑一部
        }
        //反转后半段链表
        //当链表为奇数个的时候,从low.next开始,因为low的位置只有一个不需要反转
        //当链表为偶数个的时候,从low.next开始
        singleNode<Integer> end = reverseSingleNode(low.next);//该链表的头节点为原链表最后一个,所以我们定义为end好记一些
        fast = end;//将反转后得链表连在fast上,这样我们就不用额外给他留空间了,做到了O(1)
        while (cur != null || fast != null){//都不为null得时候代表链表还有元素
            if(cur.value != fast.value){
                //非回文://由于我们没有动low指针,所以low依旧在中点,end也没动依旧是反转后的链表,end链表再反转后连上low.next即可
                return false;
            }
            cur = cur.next;
            fast = fast.next;
        }
        //while循环过后发现是回文,就是对称的,依旧可以把end连接在low.next
        low.next = reverseSingleNode(end);
        return true;
    }

问题4:将单向链表按某值划分为左边小、中间相等、右边大的形式

方法一:先遍历一下链表,统计出来链表的长度,再创建一个Node数组,将节点全部放进去,利用元素的value去跟值对比,进行荷兰国旗问题,最后再将这个数组中节点依次连起来,即可得到链表

/**
     * 将单向链表按某值划分为左边小、中间相等、右边大的形式
     * 【进阶】在实现原问题功能的基础上增加如下的要求
     *     1.调整后所有小于num的节点之间的相对顺序和调整前一样
     *     2.调整后所有等于num的节点之间的相对顺序和调整前一样
     *     3.调整后所有大于num的节点之间的相对顺序和调整前一样
     *     4.时间复杂度达到O(n),额外空间复杂度达到O(1)
     * 将单向链表按某值划分为左边小、中间相等、右边大的形式
     * 笔试的时候,申请一个Node类型的数组,将链表中的节点放进去,对数组进行荷兰问题,之后再将每一个Node串起来
     * 不考虑时间复杂度和额外空间复杂度,直接将链表元素存储再数组中,通过荷兰国旗排序方法进行分类,再将数组中元素一一组成链表--->时间复杂度O(n),额外空间复杂度O(n)
     */
    public static void swap(singleNode[] arr,int a,int b){
        singleNode temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    public static singleNode zuozhongyou1(singleNode head,int num){
        //head为null就证明链表不在,head.next为null就是链表只有一个节点也不需要排
        if(head == null || head.next == null){
            return head;
        }
        //定义一个数组将节点全部放进去
        //首先需要知道数组创建多大
        int length = 0;
        while (head != null){
            length++;
            head = head.next;
        }
        //遍历结束后length就是节点的个数
        //数组长度就是节点个数,创建数组
        singleNode[] arr = new singleNode[length];
        //依次遍历把节点放进数组
        int i = 0;
        while (head != null){
            arr[i] = head;
            head = head.next;
            i++;
        }
        //遍历结束后数组中就是全部节点了
        //对数组进行荷兰国旗问题
        helanguoqi(arr,0,length-1,num);
        //荷兰国旗之后整个数组就会分为三个区域,之后再给他们连接起来
        for (int j = 0;j < length;j++){
            arr[j].next = arr[j+1];
        }
        return arr[0];//返回头节点
    }
    public static void helanguoqi(singleNode[] arr,int L,int R,int num){
        //定义小于左边界和大于右边界
        int less = L-1;
        int more = R;
        //定义用于遍历的指针
        int i = L;//由于是从每个区域进行比较,所以起点应该为所选数组的左端点
        while (L < R && i < R-L+1){
            if((int)arr[i].value < num){
                swap(arr,++L,i++);
            }else if((int)arr[i].value > num){
                swap(arr,i,--R);
            }else {
                i++;
            }
        }
    }

方法二:定义六个节点分别为小于区的头和尾、等于区的头和尾、大于区的头和尾,之后 从头遍历到尾每遍历到一个点,都先存下来下一个节点之后把next连接断开,之后比较value和那个数,将这个节点放在这三个区域内。全放完之后三个区域连接起来返回头节点

注意:

三个区域头尾连接的时候,一定要讨论情况,因为三个区域中可能会有不存在的情况

当把一个节点放进三个区域的时候,一定要看这个节点是不是这个区域的第一个节点,之后看尾需不需要动

public static singleNode zuozhongyou2(singleNode head,int num){
        //如果链表为空或者只有一个节点返回头节点
        if(head == null || head.next == null){
            return head;
        }
        //定义六个边界
        singleNode lt_num_head = null;
        singleNode lt_num_end = null;
        singleNode eq_num_head = null;
        singleNode eq_num_end = null;
        singleNode gt_num_head = null;
        singleNode gt_num_end = null;

        while (head != null){
            //定义一个用于遍历链表的指针
            singleNode nextNode = head.next;//保存链表的下一个节点,便于我们后续将head后移,先存起来,免得断链后找不到
            head.next = null;//断链便于,否则将会返回以该节点为头节点的链表数据
            if((int)head.value < num){//当前节点的value小于num的话
                if(lt_num_head == lt_num_end){//左右区间一样,也就是里面没有数据的话
                    lt_num_head = head;
                    lt_num_end = head;
                    head = nextNode;//
                }else {//区间里有节点,所以头不动,尾动,添加新节点
                    lt_num_end.next = head;//将区间尾部的next指向当前节点——把当前节点和区间连上
                    lt_num_end = head;//区间尾部后移——也就是当前节点作为区间最后一个节点
                    head = nextNode;
                }
            }
            if((int)head.value == num){
                if(eq_num_head == eq_num_end){
                    eq_num_head = head;
                    eq_num_end = head;
                    head = nextNode;
                }else {
                    eq_num_end.next = head;
                    eq_num_end = head;
                    head = nextNode;
                }
            }
            if((int)head.value > num){
                if(gt_num_head == gt_num_end){
                    gt_num_end = head;
                    gt_num_head = head;
                    head = nextNode;
                }else {
                    gt_num_end.next = head;
                    gt_num_end = head;
                    head = nextNode;
                }
            }
            //三个区都分好,也可以把head = temp拿出来一起共用
        }

        //三条链都已经排好,现在需要将三条链连接起来
        if(lt_num_head != null){
            lt_num_end.next = eq_num_head;
            if(eq_num_end != null){
                eq_num_end.next = gt_num_head;
            }else {
                lt_num_end.next = gt_num_head;
            }
            return lt_num_head;
        }else {
            if(eq_num_head != null){
                eq_num_end.next = gt_num_head;
                return eq_num_head;
            }else {
                return gt_num_head;
            }
        }
    }

问题5:复制含有随机指针节点的链表
一种特殊结构的单链表节点类描述如下:

class Node{

    int value;

    Node next;

    Node rand;

    Node(int val){value = val;}

}

rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。(要求时间复杂度O(n),额外空间复杂度O(1))
方法一:创建一个HashSet,将链表中每个节点都放进HashSet(当做key),同时value为新创建的节点value值跟原节点相同。全放进HashSet后,之后再给新创建的节点赋值next和random。最后我们再返回新创建链表的头节点即可

/**
     * 方法一:rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。
     * 给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,
     * 并返回复制的新链表的头节点。(要求时间复杂度O(n),额外空间复杂度O(1))
     * 在不考虑时间复杂度和空间复杂度的时候
     * 直接复制,通过HashMap进行复制,先复制值,再复制其中的属性  --> 时间复杂度O(n),额外空间复杂度O(N)
     */
class Node{
        int value;
        Node next;
        Node rand;
        Node(int val){
            value = val;
        }
    }
    public singleNode random1(singleNode head){
        HashMap<singleNode,singleNode> hashmap = new HashMap<>();//定义一个哈希表,用于存放节点,key的话我们放原先链表的Node,value存放复制后的新节点
        singleNode temp = head;
        while (head != null){
            hashmap.put(temp,new singleNode(temp.value));
            temp = temp.next;//temp指针往下走
        }
        //节点都放进栈中后,指明next和random
        temp = head;
        while (temp != null){
            hashmap.get(temp).next = hashmap.get(temp.next);
            //如果我们节点带random就写下一行
            //hashmap.get(temp).random = hashmap.get(temp.random);
        }
        return hashmap.get(head);//返回复制的头节点
    }

方法二:将复制的节点连接在原节点后面,构成1-1'-2-2'-3-3'这样的结构,之后将random属性赋值给复制后的新节点,最后将这个链拆解成1-2-3和1'-2'-3'

public static SingleNode copyByHashMap2(SingleNode head){
     SingleNode temp = head;
     //在原链表的基础上构建 1->1'->2->2'->3->3'
     while(temp != null){
         SingleNode cur = temp.next;
         // 1->1'-2   复制当前节点,并且当前节点的下一个节点指向自己,复制的节点指向当前节点的下一个节点
         temp.next = new SingleNode(temp.value);
         temp.next.next = cur;
         temp = temp.next.next;
     }
     //现在新旧节点均在原来的链表上,将原链表节点的随机属性复制给新的节点
    temp = head;
     while(temp != null){
         temp.next.random = (temp.random == null) ? null : temp.random.next;
         temp = temp.next.next;
     }
     //分离出新节点,组成新链表
    SingleNode newhead = head.next;
    temp = head;
     while(temp != null){//可以先把下两个节点保存下来,
         //链表结构:1->1'->2->2'->3->3'
         SingleNode cur = temp.next; //先保存1‘
         temp.next = temp.next.next; //将 1-> 2 ,此时temp的下一个节点就是2   2->2'->3->3'
         //此时以cur为头节点的链表 : 1’->2->2'->3->3'
         cur.next = (cur.next == null) ? null : cur.next.next; //将 1'->2'
         temp = temp.next;//temp现在在节点1,应该向后指向2节点,由于上面已经将temp更新为 1->2->2'->3->3'
     }
     return newhead;
}

问题6:两个链表相交的一系列问题

给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null。时间复杂度O(n),额外空间复杂度O(1)

首先如何判断一个链表是否存在环:如果一直遍历下去能遍历到null就证明没有环,如果遍历不到null就代表有环。而且当两个链表相交之后,后面的都相同

那么如何将这个入环第一个节点取出来呢?

方法一:将链表从头开始放进HashSet中,先判断这个节点是否存在于HashSet,如果不存在就放进去并且后移,如果存在,就代表这是入环的第一个节点

//如果带环,返回入环的第一个节点
    //方法一:放进HashSet
    public static singleNode huan1(singleNode head){
        //如果没有或者是只有一个节点或者只有两个节点,都代表不是一个环所以返回null
        if(head == null || head.next == null || head.next.next == null){
            return null;
        }
        HashSet<singleNode> hashSet = new HashSet<>();
        while (head != null){
            //如果hashset中有这个节点就返回,如果没有就继续添加
            if(!hashSet.contains(head)){
                hashSet.add(head);
                head = head.next;
            }
            //有就返回
            return head;
        }
        //没有返回null
        return null;
    }
    

方法二:定义快慢指针,快指针每次走两步,慢指针每次走一步,当快慢指针相遇的时候,慢指针不动,快指针回到头节点,之后快慢指针每次走一步,相遇即为入环第一个节点

//方法二:快慢指针
    public static singleNode huan2(singleNode head){
        //定义快慢指针
        //因为我们后面要利用快慢指针相遇来判断是否有环,所以快慢指针先不能定义为head
        singleNode temp = head;//定义一个变量指针,方便后面得快指针回到开头
        singleNode fast = head.next.next;
        singleNode low = head.next;
        while (fast != low){//只要不相遇就还在追逐
            fast = fast.next.next;
            low = low.next.next;
        }
        //循环结束后代表快指针和慢指针相遇了
        //让快指针回到开头,慢指针不变,两者每次都走一步,一定会在入环节点相遇
        fast =temp;//快指针回到开头,此时慢指针还在原地
        while (fast != low){
            fast = fast.next;
            low = low.next;
        }
        return fast;//这就是入环的第一个节点
    }

两个单链,求出相交的部分

方法一:将第一个链放进HashSet中,之后遍历第二个链,当有相同的时候即为相交,返回,如果没有返回下移,直到最后返回null

/**
     * 笔试暴力方法
     * 将一个链表的节点全部放进HashSet中,之后另一个链表从头开始遍历,判断是否在HashSet中,如果没有的话就向下移动,如果有的话就代表是相交节点,直到最后
     */
    //记住:单链表只有一个next,所以脑补的很多结构都是错的
    public static singleNode xiangjiao(singleNode s1,singleNode s2){
        //将第一个链表的节点全部放进HashSet中
        HashSet<singleNode> hashSet = new HashSet<>();
        while (s1 != null){
            hashSet.add(s1);
            s1 = s1.next;
        }
        //此时已经将第一个链表的节点全部放进HashSet中
        //开始遍历第二个链表,如果HashSet中有,就代表,有重合,返回重合的头节点
        while (s2 != null){
            if(!hashSet.contains(s2)){
                s2 = s2.next;
            }
            return s2;
        }
        return null;
    }

方法二:因为两个链表相交和相交之后的内容全部相同,所以短链走x步到达该点那么长链也是走x步到达该点,所以让长链走到距离该点x步的时候,短链出发,这样当他俩相遇的时候即为相交。定义长度len1和len2分别记录链1和链2的长度,之后记录出差值,让长链先走差值步数之后一起走,相遇即为相交

/**
     * 面试方法
     * 两个无环单向链表如果相交,后面公共部分一定是共有的,所以end一定相同
     * 两个链表都是无环的情况下,分别 从头遍历到为,得到head1/2 end1/2 length1/2
     * 先判断end1/2内存地址是不是一个,如果不是一个,两表不相交,如果是一个,就代表相交
     * 之后让链表一先走(length1-length2)步数,之后再跟链表二一起走,一定会在第一个相交节点处相遇(因为相交之后公共部分长度相同,链表一又先把差走完了,所以一定相遇)
     */
    public static singleNode nohuanxiangjiao(singleNode s1,singleNode s2){//传入两个链表的头节点
        //定义两个链表的指针,用于遍历
        singleNode head1 = s1;
        singleNode head2 = s2;
        int count1 = 0;//链表1的长度
        int count2 = 0;//链表2的长度
        while (head1 != null){
            count1++;//记录长度
            head1 = head1.next;
        }
        //while循环结束时head1如果为null代表为无环链表
        while (head2 != null){
            count2++;//记录长度
            head2 = head2.next;
        }
        //while循环结束时head2如果为null代表为无环链表
        if(head1 != head2){
            return null;
            //如果head1和head2相同,则代表都是单向链表,如果不同则代表一个有环一个无环,那不可能相交
        }
        //长的需要先走(长-短)的长度之后,两个指针一起走,相同的时候就是交点
        //因为需要先走差值,需要记录两个链表的长度
        //需要判断哪个长,哪个长哪个先走
        //原来的head1/2没用了,所以把他的地址重新设置一下就好了,不需要再开辟内存
        //head1名为长的,head2名为短的
        head1 = count1 > count2 ? s1 : s2;//如果count1大那么就是第一个链表长,那么把新指针放在s1否则放到s2
        head2 = head1==s1 ? s2 : s1;
        for (int i = 0;i < Math.abs(count1);i++){
            head1 = head1.next;
        }
        //此时长的已经走完了差值
        while (head1 != null){
            if(head1 == head2){
                //返回交点
                return head1;
            }
            head1 = head1.next;
            head2 = head2.next;
        }
        //如果没有交点返回null
        return null;
    }

一个为无环链一个为有环链,不可能存在相交

两个有环链相交问题:

1.两个环不相交的话,求出来loop1之后让loop1继续走,如果再次回到loop1之前没有碰到loop2代表无相交

2.两个环相交在同一点,可能是入环第一点或者是入环前,这时候就可以把环去掉,之后转变为两个无环单链相交的问题了。 

3.两个环相交在不同点,loop1和loop2不同,但是loop1继续走走回自己之前能碰到loop2

public static singleNode youhuanxiangjiao(singleNode s1,singleNode s2){
        //定义两个链表的头节点为p1/p2
        singleNode p1 = s1;
        singleNode p2 = s2;
        singleNode loop1 = huan2(s1);//第一个链表的入环第一个节点
        singleNode loop2 = huan2(s2);//第二个链表的入环第二个节点

        //当两个入环节点为同一节点的时候,代表这这两条链入环在相同位置,可能在入环之前相交也有可能在入环那一点相交
        if (loop1 == loop2){
            //即使是在入环点相角,这个问题也可以转换成两条不含圆环链的问题
            while (p1 != loop1 && p2 != loop2){
                int len1 = 0;//第一个链到入环点的长度
                while (p1 != loop1){
                    len1++;
                    p1 = p1.next;
                }
                int len2 = 0;//第二个链到入环点的长度
                while (p2 != loop2){
                    len2++;
                    p2 = p2.next;
                }
                //此时len1和len2代表两个链不含环的长度
                //定义两个从头遍历的指针 head1时长的 head2时短的
                singleNode head1 = len1 > len2 ? s1 : s2;
                singleNode head2 = head1 == s1 ? s2 : s1;

                for(int i = 0;i < Math.abs(len1-len2);i++){
                    head1 = head1.next;
                }
                //此时head1已经走完了两个长度的差值
                while (head1 != loop1 && head2 != loop2){
                    if(head1 == head2){
                        //两者跑到同一个节点即为所求
                        return head1;
                    }
                    head1 = head1.next;
                    head2 = head2.next;
                }
                return null;
            }
        }else {
            //让loop2不动,loop1继续转去找loop2,如果相遇则返回,即代表相交,如果不能相遇返回null
            singleNode zhuan1 = loop1.next;
            while (zhuan1 != loop2){
                if (zhuan1 == loop2){
                    return loop2;//返回哪个都一样
                }
                zhuan1 = zhuan1.next;
                return null;
            }
        }
        return null;
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值