链表

239 阅读7分钟

1. 几种排序的时间、空间复杂度及稳定性

  • 选择排序:时间复杂度O(N*N);空间复杂度O(1);稳定性:无;
  • 冒泡排序:时间复杂度O(N*N);空间复杂度O(1);稳定性:有;
  • 插入排序:时间复杂度O(N*N);空间复杂度O(1);稳定性:有;
  • 归并排序:时间复杂度O(N*logN);空间复杂度O(N);稳定性:有;
  • 快排(3.0版本):时间复杂度O(N*logN);空间复杂度O(logN);稳定性:无;
  • 堆:时间复杂度O(N*logN);空间复杂度O(1);稳定性:无;
坑:有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,让面试官解。

原因:可以参考快排,快排能做到排序,那么奇偶问题也就可以。但是稳定性是无,这个题的稳定性也是无。

2. 哈希表的简单介绍

  • 哈希表在使用层面可以理解为一种集合结构
  • 如果只有key,没有伴随数据value,可以使用HashSet结构
  • 如果既有key,又有伴随数据value,可以使用HashMap结构
  • 有无伴随数据,是HashSet和HashMap唯一区别,底层的实际结构是一回事
  • 使用哈希表增(put)、删(remove)、改(put)和查(get)等操作,可以认为时间复杂度为O(1),但是常数时间比较大
  • 放入哈希表的东西,如果是基本类型,内部按值传递,内存占用就是这个东西的大小
  • 放入哈希表的东西,如果不是基本类型,内部按引用传递,内存占用就是这个东西内存地址的大小

3. 有序表的简单介绍

  • 有序表在使用层面可以理解为一种集合结构
  • 如果只有key,没有伴随数据value,可以使用HashSet结构
  • 如果既有key,又有伴随数据value,可以使用HashMap结构
  • 有无伴随数据,是HashSet和HashMap唯一区别,底层的实际结构是一回事
  • 有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织
  • 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层的具体实现不同。
  • 放入有序表的东西,如果是基本类型,内部按值传递,内存占用就是这个东西的大小
  • 放入有序表的东西,如果不是基本类型,必须提供比较器,内部按引用传递,内存占用就是这个东西内存地址的大小
  • 不管底层具体实现,只要是有序表,都有以下固定的基本功能和固定的时间复杂度

4. 有序表的固定操作

image.png

5. 判断一个链表是否是回文结构

image.png 笔试思路:(1)整条链表都放在栈中,栈是先进后出,栈出的数据与链表头位置开始比对,都符合链表是回文。(2)取链表中间位置以后的元素,放到栈中,出栈与链表比对,都符合链表是回文。

笔试code

public static boolean isPalindrome1(Node head) {
    Stack<Node> stack = new Stack<Node>();  // 准备一个栈
    Node cur = head;
    while(cur != null){  // 所以的东西都压到栈中
        stack.push(cur);
        cur = cur.next;
    }
    while(head != null){
        if(head.value != stack.pop().value){ // 栈弹出一个,只要不相等,返回false
            return false;
        }
        head = head.next;
    }
    return true;
}

n/2的方法

public static boolean isPalindrom2(Node head) {
    if(head == null || head.next == null){
        return true;
    }
    Node right = head.next;
    Node cur = head;
    while(cur.next != null && cur.next.next != null){
        right = right.next;
        cur =cur.next.next;
    }
    Stack<Node> stack = new Stack<Node>();
    while(right != null){
        stack.push(right);
        right = right.next;
    }
    while(!stack.isEmpty()){
        if(head.value != stack.pop().value){
            return false;
        }
        head = head.next;
    }
    return true;
}

面试思路: 快指针一次走两步,慢指针一次走一步,希望快指针走完的时候,慢指针指向中点的位置,中点往下的位置逆序,链表的中间位置设为null,头尾用两个引用计,分别为A、B,两个引用计往中间走,开始比对。引用计有一个走到null,停。返回true和fasle之前再把右边的部分恢复成原来的样子。

image.png 面试code

public static boolean isPalindrome3(Node head){
    if(head == null || head.next == null){
        return true;
    }
    Node n1 = head;
    Node n2 = head;
    while(n2.next != null && n2.next.next != null){
        n1 = n1.next; // 慢指针一次走一步 n1--->mid
        n2 = n2.next.next; // 快指针一次走两步,n2走完,n1会走到中点的位置 n2--->end
    }
    n2 = n1.next; // 右侧部分的第一个
    n1.next = null; // 中间指向null
    Node n3 = null;
    while(n2 != null){ // 右半部分开始逆序
        n3 = n2.next; // 储存下一个节点
        n2.next = n1; // 右侧的下一个节点转换
        n1 = n2; // n1 移动
        n2 = n3; // n2 移动
    }
    n3 = n1; // n3-->存储最后一个节点
    n2 = head; // n2--->左侧第一个节点
    boolean res = true;
    while(n1 != null && n2 != null){
        if(n1.value != n2.value){
            res = false;
            break; 
        }
        n1 = n1.next; // 左侧往中间走
        n2 = n2.next; // 右侧往中间走
    }
    n1 = n3.next;
    n3.next = null;
    while(n1 != null){ // 右侧逆序回来
        n2 = n1.next;
        n1.next = n3;
        n3 = n1;
        n1 = n2;
    }
    return res;
}

6. 链表分成左边小、中间相等、右边大的形式

image.png

image.png code:

public static Node listPartition2(Node head,int pivot){
    Node sH = null; //small head
    Node sT = null; //small tail
    Node eH = null; //equal head
    Node eT = null; //equal tail
    Node mH = null; //big head
    Node mT = null; //big tail
    Node next = null; //save next node
    while(head != null){
        next = head.next;
        head.next = null;
        if(head.value < pivot){
            if(sH == null){
                sH = head;
                sT = head;
            } esle {
                sT.next = head; // 老的尾巴下个节点指向当前节点
                sT = head; // 当前节点变成小于区域的新的尾巴
            }
        } else if(head.value == piovt){
            if(eH == null){
                eH = head;
                eT = head;
            } esle {
                eT.next = head;
                eT = head;
            }
        } else if(head.value > piovt){
            if(mH == null){
                mH = head;
                mT = head;
            } esle {
                mT.next = head;
                mT = head;
            }
        }
        head = next;
    }
    // small and equal reconnect
    if(sT != null){ // 如果有小于区域
        sT.next = eH;
        eT = eT == null ? sT : eT; // 下一步,谁去连大于区域的头,谁就变成eT
    }
    // all reconnect
    if(eT != null){ // 如果小于区域和等于区域,不是都没有
        eT.next = mH;
    }
    return sH != null ? sH : (eH != null ? eH : mH);
}

7. 复制含有随机指针节点的链表

image.png 分析图哈希表的方式:

image.png

public static Node copyListWithrand1(Node head){
    HashMap<Node, Node> map = new HashMap<Node, Node>(); // 创建hash表
    Node cur = head;
    // 作出对应关系,克隆节点也都声成了
    while(cur != null){
        map.put(cur, new Node(cur.value)); // 来到当前的cur,作出他的克隆节点
        cur = cur.next; // 每个节点都循环
    }
    cur = head;
    while(cur != null){
        // cur 老
        // map.get(cur) 新
        map.get(cur).next = map.get(cur.next); // cur.next是老的指向 
        map.get(cur).rand = map.get(cur.rand);
        cur = cur.next;
    }
    return map.get(head);
}

8. 两个无环单链表相交

image.png 入环节点: c是入环节点 image.png

找到第一个入环节点:环外5个节点环内四个节点

快慢指针:快指针走两步,慢指针走一步。快慢指针会在环上相遇。当快慢指针相遇时,快指针回到头节点,快慢指针之后都以一步的形式走,最后会在入环节点相遇。

image.png code:

public static Node getLoopNode(Node head){
    if(head == null || head.next == null || head .next.next == null){
        return null;
    }
    Node n1 = head.next; // n1--->slow
    Node n2 = head.next.next; // n2---> fast
    // 让快慢指针相遇
    while(n1 != n2){
        if(n2.next == null || n2.next.next == null){
            return null;
        }
        n2 = n2.next.next; // 快指针走两步
        n1 = n1.next; // 慢指针走一步
    }
    // 快慢指针相遇后,快指针指向头节点
    n2 = head; // n2---> walk again from head
    // 快慢指针个走一步后,快慢指针相遇
    while(n1 != n2){
        n1 = n1.next;
        n2 = n2.next;
    }
    return n1; // 返回第一个入环节点
}

两个无环单链表相交

单链表指针只会有一个next的

分析图:

image.png code:

public static Node noLoop(Node head1, Node head2){
    if(head1 == null || head2 == null){
        return null;
    }
    Node cur1 = head1;
    Node cur2 = head2;
    int n = 0;
    while (cur1.next != null){
        n++;
        cur1 = cur1.next;
    }
    while(cur2.next != null){
        n--;
        cur2 = cur2.next;
    }
    if(cur1 != cur2){
       return null;
    }
    cur1 = n > 0 ? head1 : head2; // 记录长链表,谁长,谁的头变成cur1
    cur2 = cur1 == head1 ? head2 : head1; // 如果长链表是head1,那么短链表就是head2; 谁短, 谁的头变成cur2
    n = Math.abs(n); // n 其实就是长链表和短链表的差值,再取绝对值
    // 让长链表走n步
    while(n != 0){
        n--;
        cur1 = cur1.next;
    }
    // 链表的节点不相等时,长链表也走完了n步,接着长链表和短链表一起走,就会相遇
    while(cur1 != cur2){
        cur1 = cur1.next;
        cur2 = cur2.next;
    }
    return cur1;
}

9. 两个有环单链表相交

两个链表都有环的三种情况

image.png

两个有环链表,返回第一个相交节点,如果不相交返回null

public static Node bothLoop (Node head1, Node loop1, Node head2, Node loop2){
    Node cur1 = null;
    Node cur2 = null;
    // 入环节点相同 上图中的情况2
    // 跟无环链表的处理逻辑一样(跟有环无环没有关系)
    if(loop1 == loop2){
        cur1 = head1;
        cur2 = head2;
        int n = 0;
        while(cur1 != loop1){
            n++;
            cur1 = cur1.next;
        }
        while(cur2 != loop2){
            n--;
            cur2 = cur2.next;
        }
        cur1 = n > 0 ? head1 : head2;
        cur2 = cur1 == head1 ? head2 : head1;
        n = Math.abs(n);
        // 让长链表走n步
        while(n != 0){
            n--;
            cur1 = cur1.next;
        }
        while(cur1 != cur2){
            cur1 = cu1.next;
            cur2 = cur2.next;
        }
        return cur1;
    } else {
        // cur1等于入环节点的下一个,上图中的1 入环转圈的过程
        cur1 = loop1.next;  
        // cur1在转回入环节点之前
        while(cur1 != loop1){
            // cur1遇到了cur2的入环节点
            if(cur1 == loop2){
                // 返回loop1或者loop2都行
                return loop1;
            }
            cur1 = cur1.next;
        }
        // 如果cur1都转回到自己了,还没有遇到,说明不相交
        return null;
    }
}

10. 有环无环链表的总体实现

public static Node getIntersectNode(Node head1, Node head2){
    if(head1 == null || head2 == null){
        return null;
    }
    Node loop1 = getLoopNode(head1); // head1的入环节点
    Node loop2 = getLoopNode(head2); // head2的入环节点
    if(loop1 == null && loop2 == null){
        return noLoop(head1, head2);
    }
    if(loop1 != null && loop2 != null){
        return bothLoop(head1, loop1, head2, loop2);
    }
    return null
}