每日算法总结——排序算法总结、哈希表与有序表、回文链表判断

一、排序算法总结

排序算法的稳定性

同样值的个体之间,如果不因为排序而改变相对次序,就是这个排序是有稳定性的;否则就没有。

  • 不具备稳定性的排序:
    选择排序、快速排序、堆排序
  • 具备稳定性的排序:
    冒泡排序、插入排序、归并排序、一切桶排序思想下的排序
基于比较的排序算法
排序算法时间复杂度空间复杂度是否具有稳定性
冒泡排序 O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)
选择排序 O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)
插入排序 O ( N 2 ) O(N^2) O(N2) O ( 1 ) O(1) O(1)
归并排序 O ( N ∗ l o g N ) O(N*logN) O(NlogN) O ( N ) O(N) O(N)
快速排序 O ( N ∗ l o g N ) O(N*logN) O(NlogN) O ( l o g N ) O(logN) O(logN)
堆排序 O ( N ∗ l o g N ) O(N*logN) O(NlogN) O ( l o g N ) O(logN) O(logN)
  • 一般会选择快速排序,因为虽然归并排序、快速排序、堆排序的时间复杂度都是 O ( N ∗ l o g N ) O(N*logN) O(NlogN),但实际实验指出快速排序是最快的
  • 当需要保持稳定性时,选择归并排序,其劣势是空间复杂度高

基于比较的排序,目前没有找到时间复杂度 O ( N ∗ l o g N ) O (N* logN) O(NlogN),额外空间复杂度 O ( 1 ) O(1) O(1),又稳定的排序。

常见的坑
  1. 归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,有兴趣可以搜 “归并排序内部缓存法”
  2. “原地归并排序” 的帖子都是垃圾,会让归并排序的时间复杂度变成O(N^2)
  3. 快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜 “01 .stable sort’
  4. 所有的改进都不重要,因为目前没有找到时间复杂度0 (N*logN),额外空间复杂度0(1),又稳定的排序。
  5. 有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变、时间复杂度 O ( N ∗ l o g N ) O(N*logN) O(NlogN)以下、空间复杂度 O ( l o g N ) O(logN) O(logN)以下,碰到这个问题,可以怼面试官。
    • 经典快排的partition操作无法保证稳定性,但是经典快排的partition是0/1标准,和奇偶问题是同一种调整策略,所以经典快排无法解决该问题,面试官教教我😆
工程上对排序的改进
  1. 充分利用 O ( N ∗ l o g N ) O(N*logN) O(NlogN) O ( N 2 ) O(N^2) O(N2)排序各自的优势
    • 根据数据的规模选择不同的排序,比如插入排序规模比较低的情况下,排序效率是比快排及归并排序高不少的。
    • 高级语言底层库中实现的排序(比如java的Array.sort())代码量通常是非常大的,因为它把基本的排序算法都实现了,根据数据的不同,选择不同的排序策略
  2. 稳定性的考虑
    • Array.sort(),底层对于基础类型会选择时间复杂度更好一点的排序比如快排,但对于自定义类型的排序,java不知道要不要保持稳定性,所以一般会选择稳定的排序,比如归并排序。

二、哈希表与有序表

2.1 哈希表的简单介绍
  • 哈希表在使用层面上可以理解为一种集合结构
  • 如果只有key,没有伴随数据value,可以使用HashSet结构 (C++中叫UnOrderedSet)
  • 如果既有key,又有伴随数据value,可以使用HashMap结构 (C++中叫Un0r der edMap)
  • 有无伴随数据,是HashMap和HashSet唯一的区别, 底层的实际结构是一致的
  • 使用哈希表增(put)、删(remove)、改(put)和查(get)的操作,可以认为时间复杂度为 O ( 1 ) O(1) O(1),但是常数时间比较大
  • 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西本身的大小
  • 放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是这个东西内存地址的大小
2.2 有序表的简单介绍
  • 有序表在使用层面上可以理解为一种集合结构
  • 如果只有key,没有伴随数据value,可以使用TreeSet结构 (C++中叫OrderedSet)
  • 如果既有key,又有伴随数据value,可以使用TreeMap结构 (C++中叫OrderedMap)
  • 有无伴随数据,是TreeSet和TreeMap唯一的区别,底层的实际结构是一回事
  • 有序表和哈希表的区别是,有序表把key按照顺序组织起来,而哈希表完全不组织
  • 红黑树、AVL树、size-balance-tree和跳表等都属于有序表结构,只是底层具体实现不同
  • 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用就是这个东西的大小.
  • 放入哈希表的东西,如果不是基础类型,必须提供比较器,内部按引用传递,内存占用是这个东西内存地址的大小
  • 不管是什么底层具体实现,只要是有序表,都有以下固定的基本功能和固定的时间复杂度
有序表的固定操作
  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 containsKey(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的后一个。

以上所有操作时间复杂度都是 O ( l o g N ) O(logN) O(logN) N N N为有序表含有的记录数

三、回文链表判断

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

方法一、栈

利用栈来判断,需要 N N N额外空间

public static boolean isPalindrome1(Node head) {
    Stack<Node> stack = new Stack<>();
    Node cur = head;
    while (cur != null) {
        stack.push(cur);
        cur = cur.next;
    }
    cur = head;
    while (cur != null){
        if (cur.value != stack.pop().value) {
            return false;
        }
        cur = cur.next;
    }
    return true;
}
方法二、快慢指针寻找中点

思路:利用快慢指针寻找链表中点,然后将链表的右半部分依次存储到栈中,通过双指针比较栈和链表左半部分元素是否相同。

需要 N / 2 N/2 N/2额外空间

public static boolean isPalindrome2(Node head) {
    if (head == null || head.next == null) {
        return true;
    }
    Node right = head.next;
    Node cur = head;
    // right将会指向中间节点
    while (cur.next != null && cur.next.next != null) {
        right = right.next;
        cur = cur.next.next;
    }
    Stack<Node> stack = new Stack<>();
    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,然后头部和尾部指针依次比较,比较结束后,需要将链表还原。

需要 O ( 1 ) O(1) O(1)额外空间

public static boolean isPalindrome3(Node head) {
        if (head == null || head.next == null) {
            return true;
        }
        Node n1 = head;
        Node n2 = head;
        // 寻找中点, n1->mid, n2->end
        while (n2.next != null && n1.next.next != null) {
            n1 = n1.next;
            n2 = n2.next.next;
        }
        // n2 -> 右半部分第一个点
        n2 = n1.next;
        // mid.next = null
        n1.next = null;
        Node n3 = null;
        // 右部分反转
        while (n2 != null) {
            // n3 -> save next node
            n3 = n2.next;
            n2.next = n1;
            n1 = n2;
            n2 = n3;
        }
        // 保存尾节点
        n3 = n1;
        // 头节点
        n2 = head;
        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;
    }
注意点

利用快慢指针找到的中点可能不是真正的中点

  • 对于大小为奇数的链表遍历结束后慢指针指向的就是中点

  • 对于大小为偶数的链表遍历结束后慢指针指向的是左中点(该链表没有真正的中点)

  • 要熟练掌握获取不同中点的方法(左中点、中点、右中点)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值