数据结构与算法之美笔记(一)复杂度 数组 链表

数据结构与算法之美笔记(一)复杂度 数组 链表,第1张

数据结构与算法之美笔记(一)复杂度 数组 链表 均摊时间复杂度

对一个数据结构进行一组连续 *** 作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些 *** 作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组 *** 作放在一块儿分析,看是否能将较高时间复杂度那次 *** 作的耗时,平摊到其他那些时间复杂度比较低的 *** 作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。

例子:每一次O(n)的插入 *** 作,都会跟着n-1次O(1)的插入 *** 作,所以把耗时多的那次 *** 作均摊到接下来的n-1次耗时少的 *** 作上,均摊下来,这一组连续的 *** 作的均摊时间复杂度就是O(1)。这就是均摊分析的大致思路。

数组

一种错误说法如下:

数组适合查找,查找时间复杂度为O(1) (错在查找不等于访问)

正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。


特定场景下的插入删除 *** 作
插入:如下图,需要在arr[2]的位置插入x,可以选择将arr[2]放到数组结尾,再将x放入arr[2],就不需要让cde都向后移动了


删除:如下图也,需要删除abc,可以只是加上“已删除”标记,就不需要让defgh都向前移动了


数组越界在C语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理。
但并非所有的语言都像C一样,把数组越界检查的工作丢给程序员来做,像Java本身就会做越界检查。


如果使用ArrayList,我们就完全不需要关心底层的扩容逻辑,ArrayList已经帮我们实现好了。每次存储空间不够的时候,它都会将空间自动扩容为1.5倍大小。
不过,这里需要注意一点,因为扩容 *** 作涉及内存申请和数据搬移,是比较耗时的。所以,如果事先能确定需要存储的数据大小,最好在创建ArrayList的时候事先指定数据大小。


有些时候,用数组会更合适些,作者总结了几点自己的经验。

1.Java ArrayList无法存储基本类型,比如int、long,需要封装为Integer、Long类,而装箱拆箱则有一定的性能消耗 ,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
2.如果数据大小事先已知,并且对数据的 *** 作非常简单,用不到ArrayList提供的大部分方法,也可以直接使用数组。
3.还有一个是作者个人的喜好,当要表示多维数组时,用数组往往会更加直观。比如Object[][] array;而用容器的话则需要这样定义:ArrayList< ArrayList > array。
总结一下,对于业务开发,直接使用容器就足够了,省时省力。毕竟损耗一丢丢性能,完全不会影响到系统整体的性能。但如果你是做一些非常底层的开发,比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

链表

常见的缓存淘汰策略有三种:先进先出策略FIFO(First In,First Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used)。


数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。

数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它,导致“内存不足(out of memory)”。如果声明的数组过小,则可能出现不够用的情况。这时只能再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,作者觉得这也是它与数组最大的区别。

Java中的ArrayList容器,当我们往支持动态扩容的数组中插入一个数据时,如果数组中没有空闲空间了,就会申请一个更大的空间,将数据拷贝过去,而数据拷贝的 *** 作是非常耗时的。

如果你的代码对内存的使用非常苛刻,那数组就更适合你。因为链表中的每个结点都需要消耗额外的存储空间去存储一份指向下一个结点的指针,所以内存消耗会翻倍。而且,对链表进行频繁的插入、删除 *** 作,还会导致频繁的内存申请和释放,容易造成内存碎片,如果是Java语言,就有可能会导致频繁的GC。


课后思考题:
如何判断一个字符串是否是回文字符串的问题,我想你应该听过,我们今天的思题目就是基于这个问题的改造版本。如果字符串是通过单链表来存储的,那该如何来判断是一个回文串呢?你有什么好的解决思路呢?相应的时间空间复杂度又是多少呢?

答:如果需要用到单链表的话,就先用快慢指针找到链表中点,并存下这个引用。在快慢指针移动的同时,修改它遍历的结点的next(这里应该是需要声明指向当前结点的前一个、下一个结点的引用),然后回到中点结点,从中间向两边同时遍历,不相同就return false,否则一直遍历到最边上,并返回true


练习题
力扣:206,141,21,19,876
206.反转链表(单链表反转)

递归法
一直递到最后结点然后一级级return当前结点,并将靠后结点的next赋为前一个结点
记得存储最后一个结点作为新的头结点,并将head的next赋为空


class Solution {
    private ListNode newHead;
    public ListNode reverseList(ListNode head) {
        if(head==null){
            return head;
        }
        fun(head);
        head.next=null;
        return newHead;
    }

    private ListNode fun(ListNode node){
        if(node.next==null){
            newHead=node;
            return node;
        }
        ListNode nextNode=fun(node.next);
        nextNode.next=node;
        return node;
    }
}

141. 环形链表(链表中环的检测)

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast=head,slow=head;
        boolean flag=false;
        while(fast!=null&&fast.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if(fast==slow){
                flag=true;
                break;
            }
        }
        return flag;
    }
}

21.合并链表(两个有序的链表合并)
自己写的垃圾代码

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode head=new ListNode();
        ListNode temp3=head;
        ListNode temp;
        while(list1!=null&&list2!=null){
            if(list1.val 

后来想起来尾部的链表可以一次性接过来,不需要继续循环了

if(list1!=null){
    temp3.next=list1;
}
if(list2!=null){
   temp3.next=list2;
}
return head.next;

19.删除链表的倒数第 N 个结点(删除链表倒数第n个结点)

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {    
        ListNode pre = new ListNode(0);
        pre.next = head;
        ListNode start = pre, end = pre;
        while(n != 0) {
            start = start.next;
            n--;
        }
        while(start.next != null) {
            start = start.next;
            end = end.next;
        }
        end.next = end.next.next;
        return pre.next;
    }
}

876.链表的中间结点(求链表的中间结点)
我一开始的代码

class Solution {
    public ListNode middleNode(ListNode head) {
        if(head.next==null){
            return head;
        }
        if(head.next.next==null){
            return head.next;
        }
        ListNode fast=head,slow=head;
        while(true){
            if(fast.next!=null){
                fast=fast.next;
            }else{
                break;
            }
            if(fast.next!=null){
                fast=fast.next;
            }else{
                slow=slow.next;
                break;
            }
            slow=slow.next;
        }
        return slow;
    }
}

别人的题解代码(才发现自己对快慢指针还是不太理解,题解判空的是fast和fast.next,我的是fast.next和fast.next.next)

class Solution {
    public ListNode middleNode(ListNode head) {
		ListNode fast = head;
		ListNode slow = head;
		while(fast != null && fast.next !=null){
		    fast = fast.next.next;
		    slow = slow.next;
		}
		return slow;
	}
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5717824.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-18

发表评论

登录后才能评论

评论列表(0条)

保存