算法一看就懂之「 数组与链表 」

数据结构是咱们软件开发中最基础的部分了,它体现着咱们编程的内功。大多数人在正儿八经学习数据结构的时候估计是在大学计算机课上,而在实际项目开发中,反而感受到用得很少。程序员

其实也不是真的用得少,只不过咱们在使用的时候被不少高级语言和框架组件封装好了,真正须要本身去实现的地方比较少而已。但别人封装好了不表明咱们就能够不关注了,数据结构做为程序员的内功心法,是很是值得咱们多花时间去研究的,我这就翻开书复习复习:算法

本文就先从你们最常用的「 数组 」和「 链表 」聊起。不过在聊数组和链表以前,我们先看一下数据的逻辑结构分类。通俗的讲,数据的逻辑结构主要分为两种:编程

  • 线性的:就是连成一条线的结构,本文要讲的数组和链表就属于这一类,另外还有 队列、栈 等数组

  • 非线性的:顾名思义,数据之间的关系是非线性的,好比 堆、树、图 等数据结构

知道了分类,下面咱们来详细看一下「 数组 」和「 链表 」的原理。框架

1、「 数组 」是什么?

数组是一个有限的、类型相同的数据的集合,在内存中是一段连续的内存区域。性能

以下图:学习

数组的下标是从0开始的,上图数组中有6个元素,对应着下标依次是0、一、二、三、四、5,同时,数组里面存的数据的类型必须是一致的,好比上图中存的都是数字类型。数组中的所有元素是“连续”的存储在一块内存空间中的,如上图右边部分,元素与元素之间是不会有别的存储隔离的。另外,也是由于数组须要连续的内存空间,因此数组在定义的时候就须要提早指定固定大小,不能改变。spa

  • 数组的访问:3d

    数组在访问操做方面有着独特的性能优点,由于数组是支持随机访问的,也就是说咱们能够经过下标随机访问数组中任何一个元素,其原理是由于数组元素的存储是连续的,因此咱们能够经过数组内存空间的首地址加上元素的偏移量计算出某一个元素的内存地址,以下:

    array[n]的地址 =  array数组内存空间的首地址 + 每一个元素大小*n

    经过上述公式可知:数组中经过下标去访问数据时并不须要遍历整个数组,所以数组的访问时间复杂度是 O(1),固然这里须要注意,若是不是经过下标去访问,而是经过内容去查找数组中的元素,则时间复杂度不是O(1),极端的状况下须要遍历整个数组的元素,时间复杂度多是O(n),固然经过不一样的查找算法所需的时间复杂度是不同的。

  • 数组的插入与删除:

    一样是由于数组元素的连续性要求,因此致使数组在插入和删除元素的时候效率比较低。

    若是要在数组中间插入一个新元素,就必需要将要相邻的后面的元素所有日后移动一个位置,留出空位给这个新元素。仍是拿上面那图举例,若是须要在下标为2的地方插入一个新元素11,那就须要将原有的二、三、四、5几个下标的元素依次日后移动一位,新元素再插入下标为2的位置,最后造成新的数组是:

    2三、四、十一、六、1五、五、7

    若是新元素是插入在数组的最开头位置,那整个原始数组都须要向后移动一位,此时的时间复杂度为最坏状况即O(n),若是新元素要插入的位置是最末尾,则无需其它元素移动,则此时时间复杂度为最好状况即O(1),因此平均而言数组插入的时间复杂度是O(n)

    数组的删除与数组的插入是相似的。

因此总体而言,数组的访问效率高,插入与删除效率低。不过想改善数组的插入与删除效率也是有办法的,来来来,下面的「 链表 」了解一下。

2、「 链表 」是什么?

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表中的指针连接次序实现的,通常用于插入与删除较为频繁的场景。

上图是“单链表”示例,链表并不须要数组那样的连续空间,它只须要一个个零散的内存空间便可,所以对内存空间的要求也比数组低。

链表的每个节点经过“指针”连接起来,每个节点有2部分组成,一部分是数据(上图中的Data),另外一部分是后继指针(用来存储后一个节点的地址),在这条链中,最开始的节点称为Head,最末尾节点的指针指向NULL。

「 链表 」也分为好几种,上图是最简单的一种,它的每个节点只有一个指针(后继指针)指向后面一个节点,这个链表称为:单向链表,除此以外还有 双向链表、循环链表 等。

双向链表:

双向链表与单向链表的区别是前者是2个方向都有指针,后者只有1个方向的指针。双向链表的每个节点都有2个指针,一个指向前节点,一个指向后节点。双向链表在操做的时候比单向链表的效率要高不少,可是因为多一个指针空间,因此占用内存也会多一点。

循环链表:

其实循环链表就是一种特殊的单向链表,只不过在单向链表的基础上,将尾节点的指针指向了Head节点,使之首尾相连。

  • 链表的访问

    链表的优点并不在与访问,由于链表没法经过首地址和下标去计算出某一个节点的地址,因此链表中若是要查找某个节点,则须要一个节点一个节点的遍历,所以链表的访问时间复杂度为O(n)

  • 链表的插入与删除

    也正式由于链表内存空间是非连续的,因此它对元素的插入和删除时,并不须要像数组那样移动其它元素,只须要修改指针的指向便可。

    例如:删除一个元素E:

    例如:插入一个元素:

既然插入与删除元素只须要改动指针,无需移动数据,那么链表的时间插入删除的时间复杂度为O(1)不过这里指的是找到节点以后纯粹的插入或删除动做所需的时间复杂度。

若是当前还未定位到指定的节点,只是拿到链表的Head,这个时候要去删除此链表中某个固定内容的节点,则须要先查找到那个节点,这个查找的动做又是一个遍历动做了,这个遍历查找的时间复杂度倒是O(n),二者加起来总的时间复杂度实际上是O(n)的。

其实就算是已经定位到了某个要删除的节点了,删除逻辑也不简单。以“删除上图的E节点”为例,假如当前链表指针已经定位到了E节点,删除的时候,须要将这个E节点的前面一个节点H的后继指针改成指向A节点,那么E节点就会自动脱落了,可是当前链表指针是定位在E节点上,如何去改变H节点的后续指针呢,对于“单向链表”而言,这个时候须要从头遍历一遍整个链表,找到H节点去修改其后继指针的内容,因此时间复杂度是O(n),但若是当前是“双向链表”,则不须要遍历,直接经过前继指针便可找到H节点,时间复杂度是O(1),这里就是“双向链表”至关于“单向链表”的优点所在。

3、「 数组和链表 」的算法实战?

经过上面的介绍咱们能够看到「 数组 」和「 链表 」各有优点,而且时间复杂度在不一样的操做状况下也不相同,不能简单一句O(1)或O(n)。因此下面咱们找了个经常使用的算法题来练习练习。

算法题:反转一个单链表
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        //定义一个前置节点变量,默认是null,由于对于第一个节点而言没有前置节点
        ListNode pre = null;
        //定义一个当前节点变量,首先将头节点赋值给它
        ListNode curr = head;
        //遍历整个链表,直到当前指向的节点为空,也就是最后一个节点了
        while(curr != null){
            //在循环体里会去改变当前节点的指针方向,原本当前节点的指针是指向的下一个节点,如今须要改成指向前一个节点,可是若是直接就这么修改了,那链条就断了,再也找不到后面的节点了,因此首先须要将下一个节点先临时保存起来,赋值到temp中,以备后续使用
            ListNode temp = curr.next;
            //开始处理当前节点,将当前节点的指针指向前面一个节点
            curr.next = pre;
            //将当前节点赋值给变量pre,也就是让pre移动一步,pre指向了当前节点
            pre = curr;
            //将以前保存的临时节点(后面一个节点)赋值给当前节点变量
            curr = temp;
            //循环体执行链表状态变动状况:
            //NULL<-1  2->3->4->5->NULL
            //NULL<-1<-2  3->4->5->NULL
            //NULL<-1<-2<-3  4->5->NULL
            //NULL<-1<-2<-3<-4  5->NULL
            //NULL<-1<-2<-3<-4<-5
            //循环体遍历完以后,pre指向5的节点
        }
        //完成,时间复杂度为O(n)
        return pre;
    }
}

以上,就是对「 数组与链表 」的一些思考。

码字不易啊,喜欢的话不妨转发朋友吧。😊

相关文章
相关标签/搜索