存储结构
数组
-
优点
- 构建方便
- 能在O(1)时间根据索引访问某个元素,快。
- 按索引遍历数组方便
-
缺点
- 构建时必须分配一段连续的空间,大小固定。
- 根据内容查找元素需要遍历数组,比较慢
- 增删元素效率低,因为要移动一部分元素
- 只能存储一种类型的数据
- 未封装任何方法,操作需要用户自己定义
-
应用场景
- 需要定义数量确定的多个相同类型的变量时
- 查询、遍历使用比较多的场景
-
例题
- 数组反转
使用首尾两个下标进行循环交换,直到从首部开始的下标大于等于数组长度的一半 - 查找符合条件的某个值
遍历,如果是有序数组,则可以使用二分查找来加快速度 - 各种排序
- 数组反转
链表
-
优点
- 创建时不用分配连续的内存空间
- 能在O(1)时间内增删元素,快。前提是该元素的前一个元素已知,双链表则已知后一个元素也行
- 大小不固定,可动态改变大小,按需增减结点
-
缺点
- 查找数据效率低,查找第k个元素需要O(k)时间
-
应用场景
- 数据元素不确定,经常进行数据的添加和删除
-
例题(单链表)
- 链表翻转
就地翻转与头结点翻转法(使用一个头结点,两个指针) - 查找倒数第k个元素
使用两个指针,第一个先走k步,第二个再开始一起走,直到末尾 - 判断是否有环
使用快慢指针(快指针一次走两步,慢指针一次一步),当快指针追上慢指针时即有环 - 判断环的大小
当快慢指针(快二慢一)第二次相遇时,快指针比慢指针多走一个环 - 给出头结点与指定结点指针,在O(1)时间内删除该节点
一般情况是遍历找到指定结点的前一个节点,然后再删除,时间为O(n)
此处可以将指定结点的数据与其后一个结点的数据交换,再将后一个结点删除,同样可以达到删除节点的效果,如果指定结点是最后一个,那按一般方法来,则总的时间复杂度依旧是O(1) - 寻找两个链表的第一个公共结点
先比较两个链表的长度m、n,然后两个指针分别指向两个链表的头结点,长的链表先走 |m-n| 步,再判断两个指针指向的节点是否一样 - 合并有序链表
创建一个新链表指针,比较需要合并的链表结点,使用新链表指针指向符合条件的结点即可。注意判断是否到结尾。 - 复制复杂链表(每个结点除了一个指向下一个结点的next指针外,还有另外一个指向任意结点的sibling指针)
解法 一:复制原始链表上的每一个结点,并用Next结点链接起来;设置每个结点的sibling结点指针。O(n2)
二:同一,但复制时把每个结点的的sibling配对信息放到一个哈希表中,设置sibling指针时查询一下配对信息即可。O(n)
三:复制原始链表的结点,并将节点放置在原结点后(如A->B => A->A1->B);再设置复制得到的节点的sibling指针(如:A1.sibling = A.sibling.next);最后将奇数位的结点与偶数位的结点拆分成两个链表,奇数位是原始链表,偶数位是复制得到的链表。
- 链表翻转
逻辑结构
栈
-
特点:后进先出(LIFO),可用数组或链表实现。
-
应用场景:只关心最近一次操作,能向前查找前一步操作
-
例题:
- 颠倒顺序,此处要求原始栈的顺序颠倒,而不是输出一个新栈
方法有使用一或二个辅助栈、递归等,具体看链接,讲的很好,侵删-.- - 最小栈问题,添加一个min函数,能够得到栈的最小元素。要求函数min、push以及pop的时间复杂度都是O(1)。
使用一个辅助栈来同步记录当前栈中最小元素的位置,每当元素进栈或者出栈时,更新辅助栈(即进栈一个记录位置的元素或出栈一个元素)。 - push、pop序列,给出两个序列(设每个序列内的元素各不相同),其中一个序列表示栈的push序列,判断另一个序列有没有可能是push序列对应的pop序列。
将push序列元素依次压进栈;同时比较栈顶元素是否与pop序列的第一个元素相同,是则出栈,再判断pop序列的下一个元素;若不相同或栈已空,则继续将下一个push序列元素进栈;重复以上操作,直到push序列都已进栈完毕。然后判断此时的栈是否为空即可。 - 用栈实现队列
使用两个栈(设为 stackA 和 stackB )来实现队列,stackA负责入栈操作,stackB负责出栈。
入栈:若stackA未满,则入栈 stackA.push;
若stackA已满,则判断stackB是否是空的,如果是空的,则将stackA的元素出栈并入栈到stackB,再将元素入栈stackA;如果stackB非空,则报错提示 “栈(队列?)已满” ,或使用更多的栈来负责出栈。。。
出栈:若stackB为空,则将stackA中的元素全部压入stackB,然后在出栈stackB栈顶元素。
若stackB非空,则直接出栈。
- 颠倒顺序,此处要求原始栈的顺序颠倒,而不是输出一个新栈
队列
-
特点:先进先出(FIFO)
-
应用场景:
-
例题:
-
滑动窗口最大值(最小值同理可得),转自LeetCode
一、暴力破解:最简单直接的方法是遍历每个滑动窗口,找到每个窗口的最大值,使用一个数组记录每个窗口出现的最大值。一共有 N - k + 1 个滑动窗口,每个有 k 个元素,故需要N-k+1大小的数组来辅助记录,于是算法的时间复杂度为 O(Nk),空间复杂度为O(N-k+1)表现较差。
二、双端队列(deque):- 处理前 k 个元素,初始化双端队列。 遍历整个数组。
- 在每一步 :清理双端队列 : 只保留当前滑动窗口中包含的元素的索引; 移除比当前元素小的所有元素,它们不可能是最大的。
- 将当前元素添加到双向队列中。
- 将 deque[0] 添加到输出中。
- 返回输出数组。
- 时间复杂度:O(N),每个元素被处理两次- 其索引被添加到双向队列中和被双向队列删除。
- 空间复杂度:O(N),输出数组使用了 O(N−k+1)空间,双向队列使用了 O(k)。
三、动态规划
- 将测试数组 nums 划分为多个包含k个元素的块,若数组长度n%k!=0,则最后一个块包含的元素少于k。
- 建立两个数组left和right,长度为n。
- 使用for循环遍历整个测试数组(窗口滑动):
left[ i ](此时窗口从左往右滑动)记录窗口滑动到测试数组第 i 个位置时窗口中的最大值,但当 i % k == 0;时,证明窗口滑动到了新的块中,此时left[ i ] 记录值应为测试数组第 i 个值。right数组则是记录窗口从右往左滑动的最大值
if (i % k == 0) left[i] = nums[i]; else left[i] = Math.max(left[i - 1], nums[i]);
解析:窗口滑动时,会出现两种情况:一是窗口在块中滑动,这时left或者right数组记录的都是窗口中的最大值;二是窗口跨越了两个块,此时left记录窗口中右边那个块中的最大值,而right则记录左边那个块中的最大值。所以,结果数组就取left和right中的最大值就行。
for (int i = 0; i < n - k + 1; i++) output[i] = Math.max(left[i + k - 1], right[i]);
-
用队列实现栈
一、 使用一个队列来实现- 入栈:直接入队即可;
- 出栈:假设此时队列中现有元素个数为n,则依次将队列中的n-1个元素出队再入队,完成后,队尾元素就变成了队头元素,然后出队即可。
二、 使用两个队列来实现 - 入栈:判断两个队列是否为空,把元素放进非空的队列中(空队列用来进行出栈操作);若全为空队列,则随便放进一个队列中;
- 出栈:将非空队列中的 n-1 个元素依次出队,并入队到另一个队列(空的)中,然后将第 n 个元素出队即可。
-