数据结构
在学习数据结构之前先思考两个问题:
1、什么是数据结构 ?数据结构分为哪几种 ?
数据结构是计算机存储,组织数据的方式。
- 线性结构 (数组、链表、栈、队列、哈希表也叫散列表)
- 特点: 有索引。
- 树形结构(二叉树、红黑树、B树、堆等)
- 图形结构(邻接矩阵、邻接表)
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出来在堆上的,直接增加内存,数组的内存就不连续了。
-
增加元素。 数组增加元素分为两种情况
- 动态数组预分配长度够用。 直接在后面添加就可以,时间负责度为 O(1)
- 动态数组长度不够了。 这就麻烦了。需要重新为整个数组开辟内存。他的时间复杂度是 O(n)空间复杂度也是 O(n)
-
删除 删除和添加是比较类似的操作。
- 如果删除的最后一个元素,那么直接删除最后一个就可以,他的时间复杂度是O(1)
- 如果删除的是第一个元素呢 ? 那就麻烦了。 第一个元素删除之后,后面所以的元素都要往前移动。 所以他的时间复杂度是O(n)
-
查找
查找是数组最大的优势。因为他的内存是连续的。 例如我们想找数组中的第五个元素,我们可以直接使用数组的第一个元素的地址 +5 就可以找到 。 所以他的时间复杂度是O(1)
-
修改
修改和查找类似,先查到对应的元素,然后直接修改,所以他的复杂度 也是O(1)
链表
这里说的链表就是指单向链表 特点:
- 内存不连续
- 第一个节点,会有一个指针指向下一个节点。链表的指针指向第一个节点。最后一个节点的内存地址是空。
那他在处理数据的时候有什么特点。
-
增加元素
- 如果加在第一个位置。那他就比较简单直接更换指针添加就可以。 时间复杂度是O(1)
- 如果是添加最后一个位置,那需要链表查找到最后一个元素。这个查找的时间复杂度就是O(n),然后修改指针。
- 所以他最好的情况复杂度 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
哈希表
哈比表结构比较复杂,后面会单独介绍,但是从他的结构上面来说是属于线性结构。