数据结构(1) 复杂度和线性结构

328 阅读6分钟

数据结构

在学习数据结构之前先思考两个问题:
1、什么是数据结构 ?数据结构分为哪几种 ?

数据结构是计算机存储,组织数据的方式。

  • 线性结构 (数组、链表、栈、队列、哈希表也叫散列表)
    • 特点: 有索引。
  • 树形结构(二叉树、红黑树、B树、堆等)
  • 图形结构(邻接矩阵、邻接表)

2、我们怎么使用最单的的方式,来表示某一个数据结构在某一个应用场景的效率如何?

使用算法复杂度来表示。

复杂度

复杂度大致可以分为两种。

  1. 空间复杂度 :执行算法需要消耗多少内存
  2. 时间复杂度 :执行算法执行了多少行代码。就是执行的次数。

大O表示法。

大O表示法就是用最单的方式来表示一个算法的好坏。基本上遵从以下的几个原则。

N无限放大,忽略常数、系数、低阶。

例如 如果一个程序执行了一下几个次数他最终使用大O应怎么表示

  • 10次 => O(1)
  • n次 => O(n)
  • 2n + 3 => O(n)
  • n² => O(n²)
  • n³ + 3n² => O(n³)

来几个例子吧 :

void normalFunc() {
    int a = 0;
    a = 1 + 2;
    a = a + 3;
}

这里的这个函数执行了三次。 因为大O表示法,只要能使用常数表示的都是用O(1)表示。 所以他的时间复杂度就是O(1)

void reverseString(char *s) {
    for (int i = 0; i < strlen(s); i++) {
        char startChar = s[i];
        char endChar = s[strlen(s) - i - 1];
        if (i >= strlen(s) - i - 1) {
            printf("  %s ", s);
            break;
        } else {
            s[strlen(s) - i - 1] = startChar;
            s[i] = endChar;
        }
    }
}

上面是一个C语言的字符串反转的算法。 很明显这个程序需要执行。n (这里的n就是字符串长度) 乘一个常数次。 我们假设这个常数为8,那他执行的次数就是 8n 次。 使用大O表示法,因为他忽略常数的原则。那他的时间复杂度是 O(n) 次。

线性结构

数组

数组,大家用的最多的一种数据结构。 他有以下几个特点

  • 顺序存储。
  • 内存地址连续
  • 空间固定 很容易理解,例如:我们现在创建一个长度为10的不可变数组,他会在内存中开辟一个段连续的内存空间来存储10个元素。

那可变数组呢 ? 内存空间也是连续的吗 ?

是的,长度也是固定的。 先默认开辟一段连续的内存,当长度不够的时候,会开辟一块新的内存。如果直接在就数组上面增加内存,内存会不连续。 因为我们的数组是 new 或者 alloc出来在堆上的,直接增加内存,数组的内存就不连续了。

  • 增加元素。 数组增加元素分为两种情况

    1. 动态数组预分配长度够用。 直接在后面添加就可以,时间负责度为 O(1)
    2. 动态数组长度不够了。 这就麻烦了。需要重新为整个数组开辟内存。他的时间复杂度是 O(n)空间复杂度也是 O(n)
  • 删除 删除和添加是比较类似的操作。

    1. 如果删除的最后一个元素,那么直接删除最后一个就可以,他的时间复杂度是O(1)
    2. 如果删除的是第一个元素呢 ? 那就麻烦了。 第一个元素删除之后,后面所以的元素都要往前移动。 所以他的时间复杂度是O(n)
  • 查找

    查找是数组最大的优势。因为他的内存是连续的。 例如我们想找数组中的第五个元素,我们可以直接使用数组的第一个元素的地址 +5 就可以找到 。 所以他的时间复杂度是O(1)

  • 修改

    修改和查找类似,先查到对应的元素,然后直接修改,所以他的复杂度 也是O(1)

链表

这里说的链表就是指单向链表 特点:

  • 内存不连续
  • 第一个节点,会有一个指针指向下一个节点。链表的指针指向第一个节点。最后一个节点的内存地址是空。

那他在处理数据的时候有什么特点。

  • 增加元素

    1. 如果加在第一个位置。那他就比较简单直接更换指针添加就可以。 时间复杂度是O(1)
    2. 如果是添加最后一个位置,那需要链表查找到最后一个元素。这个查找的时间复杂度就是O(n),然后修改指针。
    3. 所以他最好的情况复杂度 O(1) 最坏的情况是 O(n),平均是 O(n)
  • 删除元素

    和增加原理一样。 必须要先查找在删除 所以他最好的情况复杂度 O(1) 最坏的情况是 O(n),平均是 O(n)

  • 修改元素 同上

  • 查找元素

    查找,上面已经提到了。 看你查找元素所在位置。 最好的情况复杂度 O(1) 最坏的情况是 O(n),平均是 O(n)

双向链表

list含有属性 size,first,last . 可以根据长度从first或者last开始遍历第一个节点的 prev是null

  • 双向链表清空,只需要帮last first = null;Java是垃圾回收,会直接回收,,OC就不知道了。(但是源码是Java销毁的,因为他有一个迭代器)
  • 删除比单向链表。几乎节省一半的操作。虽然复杂度一样(iOS自动释放池底部实现原理就是 双向链表)
  • 双向循环列表:第一个节点的prev是最后一个。 最后一个的next是第一个。

总结:

频繁插入操作,选择双向链表(插入在最后的话选择谁都可以)。频繁的查询操作选择动态数组。

单向链表 ? 哈希表的设计中用到了单向链表

特殊的线性结构,只能在一端操作。类似一个杯子。

  • 添加 push 入栈 pop 出栈。
  • 应用:
    • 浏览器的前进后退。 使用两个栈来处理的。因为他不仅可以后退,而且可以前进。( 只要输入新的就帮第二栈清空)
    • ios的 viewController 一个栈来处理的。
    • 类似的没图秀秀。撤销,恢复功能。

队列

特殊的线性表,只能在头尾操作。只能从队尾添加,队头出去。(FIFO first in first out)

就像排队取钱一样。

  • 内部可以使用双向链表进行封装。 因为双向列表,头尾操作比较方便。他本身就有 first 和 last 指针。 这样复杂度最低
  • 如果底层用栈封装。那就用栈来封装。
    • 出队的时候 判断 outStack 是否有值。如果没有。则帮intStack东西全部移出去
    • 如果有值,直接出栈
  • 也可以使用动态数组实现。动态数组实现的就是循环队列。
  • 双端队列Deque。 可以在两端 删除添加
  • 循环队列 Cycle Queue

哈希表

哈比表结构比较复杂,后面会单独介绍,但是从他的结构上面来说是属于线性结构。