算法与数据结构_4_稳定性、排序算法总结、综合排序、哈希表、有序表

  1. 1. 稳定性
  2. 2. 排序算法总结
  3. 3. 综合排序
  4. 4. 哈希表、有序表
    1. 4.0.1. 哈希表
    2. 4.0.2. 有序表
  • 5. 链表

  •  

    稳定性

    • 值排完序后相同的值,次序不变

    排序算法总结

               时间复杂度    空间复杂度    稳定性 
     选择排序    O(N^2)        O(1)         x   
     冒泡排序    O(N^2)        O(1)         √   
     插入排序    O(N^2)        O(1)         √   
     归并排序  O(N * logN)     O(N)         √   
     随机快排  O(N * logN)    O(logN)       x   
     堆排序    O(N * logN)     O(1)         x  
    

    总结:

    • 一般来讲,选择快排,因为参数项较优
    • 空间实在不够时用堆排
    • 需要稳定性时用归并

    • 基于比较的排序,目前找不到时间复杂度在O(N * logN)以下的
    • 基于比较的排序,在时间复杂度O(N * logN)时,目前找不到空间复杂度做到O(N)以下且稳定

    常见的坑:

    • 归并排序的额外空间复杂度可以变成O(1),非常难不需要掌握,搜“归并排序 内部缓存法”,那时稳定性会丧失,那为什么不用堆排呢
    • “原地归并排序”的帖子都是垃圾,它让额外空间复杂度变成O(1),但时间复杂度变成O(N^2),那为什么不用插入呢
    • 快排可以做到稳定性,非常难不需要掌握,搜“01 stable sort”,做到的同时会让额外空间复杂度变成O(N),那为什么不用归并呢
    • 所有的改进都不重要
    • 有一道题目,一个整形数组,请你做到奇数放到左边,偶数放到右边,还要求奇数之间的相对次序不变,偶数之间的相对次序不变,要求额外空间复杂度O(1),还做到时间复杂度O(N)。这是面试官在搞你。因为分奇偶相当于快排的partition,做不到稳定性。

    综合排序

    • 根据样本量 和 各自的排序算法的优势 选择不同的排序方式

    工程上对排序的改进

    • 充分利用O(N * logN)和O(N^2)排序各自的优势。如<60用插入,>=60用归并
    • 稳定性的考虑。如基础类型用快排,自定义类型用归并

    哈希表、有序表

    哈希表
    • 在C++中叫 : UnOrderedMap / UnSortedMap 、 UnOrderedSet / UnSortedSet
    • 在Java中叫 : HashSet / HashMap
    • Map、Set
      • Map : Key -> Value
      • Set : Value
      • 唯一的区别是 有无伴随数据

    Java示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    HashSet<Integer> hashSet1 = new HashSet<>();
    hashSet1.add(3); // add 添加
    System.out.printf(hashSet1.contains(3)); // contains 是否包含
    hashSet1.remove(3); // remove 删除
    System.out.printf(hashSet1.contains(3));

    HashMap<Integer, String> mapTest = new HashMap<>();
    mapTest.put(1, "jia"); // 添加
    mapTest.put(1, "yi"); // 覆盖
    mapTest.put(2, "2"); // 添加

    System.out.printf(mapTest.containsKey(1));
    System.out.printf(mapTest.get(1));

    mapTest.remove(2);
    System.out.printf(mapTest.get(2)); // null
    • 哈希表在使用(增删改查)时,时间复杂度都认为是常数级别(比较大的常数)O(1),且跟数据量无关
    • 哈希表在使用层面上可以理解为一种集合结构
    • 放入哈希表的东西,是基础类型(Java中包括String),按值传递,一律拷贝一份放表里;
      不是基础类型,按引用传递,内存占用是这个东西内存地址大小,一律8字节
    有序表
    • 在C++中叫 : OrderedMap / SortedMap 、 OrderedSet / SortedSet
    • 在Java中叫 : TreeSet / TreeMap

    Java示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    TreeMap<Integer, String> treeMap1 = new TreeMap<>();
    treeMap1.put(7, "我是7");
    treeMap1.put(2, "我是2");
    treeMap1.put(9, "我是9");

    System.out.printf(treeMap1.containsKey(7));
    System.out.printf(treeMap1.get(7));
    System.out.printf(treeMap1.firstKey() + "我最小");
    System.out.printf(treeMap1.lastKey() + "我最大");
    System.out.printf(treeMap1.floorKey(8) + "在表中<=8,且离8最近");
    System.out.printf(treeMap1.ceilingKey(8) + "在表中>=8,且离8最近");
    treeMap1.remove(7);
    • 有序表把key按照顺序组织起来
    • 性能上比哈希表差一点,所有操作都是O(logN)级别的
    • 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
    • 放入有序表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小
      如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是内存地址大小

    链表

    • 单链表、双链表结构

    题目 : 给定一个单链表的头节点head,请判断该链表是否为回文结构。
    例子 : 1->2->1,返回true; 1->2->2->1,返回true; 1->2->3,返回false。
    要求 : 如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1)。

    方法一 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 准备一个栈,所有东西压到栈里去,从头遍历,每一次比较head和栈里弹出的,有一次不同就返回false,每一步都一样返回true
    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)
    {
    return false;
    }
    head = head.next;
    }
    return true;
    }

    方法二(快慢指针) :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    public static boolean isPalindrome2(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; // 慢指针一次走一步,最终走到中点
    n2 = n2.next.next; // 快指针一次走两步,最终走到结束
    }

    // 从n1开始做逆序
    // 1->2->3->3->2->1
    // 1->2->3 3<-2<-1
    // ↓
    // null
    // 1->2->3->4->3->2->1
    // 1->2->3->4 3<-2<-1 (慢指针n1最初指向3)
    // ↓
    // null
    n2 = n1.next; // n2 -> right part first node
    n1.next = null; // mid.next -> null
    Node n3 = null;
    while (n2 != null)
    {
    n3 = n2.next; // save next node
    n2.next = n1; // next of right node convert
    n1 = n2; // n1 move
    n2 = n3; // n2 move
    }
    n3 = n1; // n3 -> save last node 从后往前走
    n2 = head; // n2 -> left first node 从前往后走
    boolean res = true;
    while (n1 != null && n2 != null)
    {
    if (n1.value != n2.value)
    {
    res = false;
    break;
    }
    n1 = n1.next; // left to mid
    n2 = n2.next; // right to mid
    }
    n1 = n3.next;
    n3.next = null;
    while (n1 != null) // recover list
    {
    n2 = n1.next;
    n1.next = n3;
    n3 = n1;
    n1 = n2;
    }
    return res;
    }

    将单向链表按某值划分成左边小、中间相等、右边大的形式

    题目 : 给定一个单链表的头节点head,节点的值类型是整形,再给定一个整数pivot。实现一个调整链表的函数,将链表调整成左部分都是值小于pivot的节点,中间部分都是值等于pivot的节点,右部分都是值大于pivot的节点。

    进阶要求 : 再实现原问题功能的基础上增加如下要求

    1. 调整后三部分里所有节点之间的相对顺序和调整前一样
    2. 时间复杂度达到O(N),额外空间复杂度达到O(1)

    笔试做法 :

    申请一个Node类型数组,把每一个值放进去,在这个数组上patition(没稳定性)

    面试做法 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    // 不用额外的数据结构,用有限的几个变量搞定
    // 6个变量,<部分的头、尾 =部分的头、尾 >部分的头、尾 初始值为null
    // 遍历,发现第一个,头和尾都指向这个数,再发现一个,尾改成这个
    // 最后3部分串起来,注意空指针,注意边界

    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 bH = null; // big head
    Node bT = 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;
    }
    else
    {
    sT.next = head;
    sT = head;
    }
    }
    else if (head.value == pivot)
    {
    if (eH == null)
    {
    eH = head;
    eT = head;
    }
    else
    {
    eT.next = head;
    eT = head;
    }
    }
    else
    {
    if (bH == null)
    {
    bH = head;
    bT = head;
    }
    else
    {
    bT.next = head;
    bT = head;
    }
    }
    head = next;
    }
    if (sT != null) // 如果有小于区域
    {
    sT.next = eH;
    eT = eT == null ? sT : eT; // 谁去连大于区域的头谁就变成eT
    }
    // 上面的if 不管跑了没有,et
    if (eT != null) // 如果小于区域和等于区域不是都没有
    {
    eT.next = bH;
    }
    return sH != null ? sH : (eH != null ? eH : bH);
    }


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

    题目 : 一种特殊的单链表节点类描述如下
    class Node
    {
    int value;
    Node next;
    Node rand;
    Node(int val)
    {
    value = val;
    }
    }
    rand指针是单链表节点结构中新增的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。

    要求 : 时间复杂度O(N),额外空间复杂度O(1)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 如果用额外空间,用哈希表
    // key(老节点) value(新节点)
    // 第二部设置新节点的next、rand指针,通过map

    public static Node copyListWithRand1(Node head)
    {
    HashMap<Node, Node> map = new HashMap<Node, Node>();
    Node cur = head;
    while (cur != null)
    {
    map.put(cur, new Node(cur.value));
    cur = cur.next;
    }
    cur = head;
    while (cur != null)
    {
    // cur老
    // map.get(cur) 新
    map.get(cur).next = map.get(cur.next);
    map.get(cur).rand = map.get(cur.rand);
    cur = cur.next;
    }
    return map.get(head);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    // 不用哈希表
    // 把克隆节点就放在老链表中该老节点的下一个,
    // 克隆节点去串上老链表的这个节点
    // 如1->1’->2->2’->null
    // 这时设置新节点的next、rand指针都为老节点的next + 1、rand + 1位置
    // 最后在next位置上把新链表分离开来

    public static Node copyListWithRand2(Node head)
    {
    if (head == null)
    {
    return null;
    }
    Node cur = head;
    Node next = null; // tmp node
    // copy node and link to every node
    // 1 -> 2
    // 1 -> 1’ -> 2
    while (cur != null)
    {
    next = cur.next;
    cur.next = new Node(cur.value);
    cur.next.next = next;
    cur = next;
    }
    cur = head;
    Node curCopy = null;
    // set copy node rand
    while (cur != null)
    {
    next = cur.next.next;
    curCopy = cur.next;
    curCopy.rand = cur.rand != null ? cur.rand.next : null;
    cur = next;
    }
    Node res = head.next;
    cur = head;
    // split
    while (cur != null)
    {
    next = cur.next.next;
    curCopy = cur.next;
    cue.next = next;
    curCopy.next = next != null ? next.next : null;
    cur = next;
    }
    return res;
    }

    题目 :
    给定两个可能有环也可能无环的单链表,头节点head1和head2.请实现一个函数,如果两个链表相交,请返回相交的 第一个节点;如果不相交,返回null。
    要求 :
    如果两个链表长度之和为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)。

    法一 哈希表:
    使用一个哈希表(Set),每往下走一个节点,查询一遍该节点是否已存在哈希表中,直到next走到null或相同为止

    法二 快慢指针:
    如果快慢指针能相遇,则有环,这时让快指针回到head,两指针一起一步一步走,会在环的第一个节点相遇

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    // 找到链表第一个入环节点,如果无环,返回null
    public static Node getLoopNode(Node head)
    {
    if (head == null || head.next == null || head.next.next == null)
    {
    return null;
    }
    Node n1 = head.next; // 慢指针
    Node n2 = head.next.next; // 快指针
    while (n1 != n2)
    {
    if (n2.next == null || n2.next.next == null)
    {
    return null;
    }
    n2 = n2.next.next;
    n1 = n1.next;
    }

    n2 = head; // walk again
    while (n1 != n2)
    {
    n1 = n1.next;
    n2 = n2.next;
    }
    return n1;
    }

    // 情况1 : loop1 == null , loop2 == null
    // 两个无环单链表,如果相交,从相交的时刻走到最后一定相同
    // 链表1 : head1 ...len1(假设长度为100)... end1
    // 链表2 : head2 ...len2(假设长度为80)... end2
    // 先判断end1和end2是否相同,如果相同,则有相交
    // 长链表先走差值步(假设为100-80=20),再两链表一起走,一定会共同迈到交叉节点

    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;
    }
    // n : 链表1长度-链表2长度
    cur1 = n > 0 ? head1 : head2; // 谁长,谁的头变成cur1
    cur2 = cur1 == head1 ? head2 : head1; // 谁短,谁的头变成cur2
    n = Math.abs(n);
    while (n != 0)
    {
    n--;
    cur1 = cur1.next;
    }
    while (cur1 != cur2)
    {
    cur1 = cur1.next;
    cur2 = cur2.next;
    }
    return cur1;
    }

    // 情况2 : loop1、loop2有一个为null,即一个有环一个无环
    // 情况3 : 两个链表都有环
    // 情况3分为3种情况
    // 1) 两个链表不相交
    // 2) 两个链表相交,且入环节点相同
    // 3) 两个链表相交,且入环节点不同

    public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2)
    {
    Node cur1 = null;
    Node cur2 = null;
    if (loop1 == loop2) // 情况2
    {
    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);
    while (n != 0)
    {
    n--;
    cur1 = cur1.next;
    }
    while (cur1 != cur2)
    {
    cur1 = cur1.next;
    cur2 = cur2.next;
    }
    return cur1;
    }
    else
    {
    cur1 = loop1.next;
    while (cur1 != loop1)
    {
    if (cur1 == loop2)
    {
    return loop1;
    }
    cur1 = cur1.next;
    }
    return null;
    }
    }

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