常见的数据结构(数组、链表、栈、队列)

存储结构

数组

  • 优点

    1. 构建方便
    2. 能在O(1)时间根据索引访问某个元素,快。
    3. 按索引遍历数组方便
  • 缺点

    1. 构建时必须分配一段连续的空间,大小固定。
    2. 根据内容查找元素需要遍历数组,比较慢
    3. 增删元素效率低,因为要移动一部分元素
    4. 只能存储一种类型的数据
    5. 未封装任何方法,操作需要用户自己定义
  • 应用场景

    1. 需要定义数量确定的多个相同类型的变量时
    2. 查询、遍历使用比较多的场景
  • 例题

    1. 数组反转
      使用首尾两个下标进行循环交换,直到从首部开始的下标大于等于数组长度的一半
    2. 查找符合条件的某个值
      遍历,如果是有序数组,则可以使用二分查找来加快速度
    3. 各种排序

链表

  • 优点

    1. 创建时不用分配连续的内存空间
    2. 能在O(1)时间内增删元素,快。前提是该元素的前一个元素已知,双链表则已知后一个元素也行
    3. 大小不固定,可动态改变大小,按需增减结点
  • 缺点

    1. 查找数据效率低,查找第k个元素需要O(k)时间
  • 应用场景

    1. 数据元素不确定,经常进行数据的添加和删除
  • 例题(单链表)

    1. 链表翻转
      就地翻转与头结点翻转法(使用一个头结点,两个指针)
    2. 查找倒数第k个元素
      使用两个指针,第一个先走k步,第二个再开始一起走,直到末尾
    3. 判断是否有环
      使用快慢指针(快指针一次走两步,慢指针一次一步),当快指针追上慢指针时即有环
    4. 判断环的大小
      当快慢指针(快二慢一)第二次相遇时,快指针比慢指针多走一个环
    5. 给出头结点与指定结点指针,在O(1)时间内删除该节点
      一般情况是遍历找到指定结点的前一个节点,然后再删除,时间为O(n)
      此处可以将指定结点的数据与其后一个结点的数据交换,再将后一个结点删除,同样可以达到删除节点的效果,如果指定结点是最后一个,那按一般方法来,则总的时间复杂度依旧是O(1)
    6. 寻找两个链表的第一个公共结点
      先比较两个链表的长度m、n,然后两个指针分别指向两个链表的头结点,长的链表先走 |m-n| 步,再判断两个指针指向的节点是否一样
    7. 合并有序链表
      创建一个新链表指针,比较需要合并的链表结点,使用新链表指针指向符合条件的结点即可。注意判断是否到结尾。
    8. 复制复杂链表(每个结点除了一个指向下一个结点的next指针外,还有另外一个指向任意结点的sibling指针)
      解法 一:复制原始链表上的每一个结点,并用Next结点链接起来;设置每个结点的sibling结点指针。O(n2)
      二:同一,但复制时把每个结点的的sibling配对信息放到一个哈希表中,设置sibling指针时查询一下配对信息即可。O(n)
      三:复制原始链表的结点,并将节点放置在原结点后(如A->B => A->A1->B);再设置复制得到的节点的sibling指针(如:A1.sibling = A.sibling.next);最后将奇数位的结点与偶数位的结点拆分成两个链表,奇数位是原始链表,偶数位是复制得到的链表。

逻辑结构

  • 特点:后进先出(LIFO),可用数组或链表实现。

  • 应用场景:只关心最近一次操作,能向前查找前一步操作

  • 例题:

    1. 颠倒顺序,此处要求原始栈的顺序颠倒,而不是输出一个新栈
      方法有使用一或二个辅助栈、递归等,具体看链接,讲的很好,侵删-.-
    2. 最小栈问题,添加一个min函数,能够得到栈的最小元素。要求函数min、push以及pop的时间复杂度都是O(1)。
      使用一个辅助栈来同步记录当前栈中最小元素的位置,每当元素进栈或者出栈时,更新辅助栈(即进栈一个记录位置的元素或出栈一个元素)。
    3. push、pop序列,给出两个序列(设每个序列内的元素各不相同),其中一个序列表示栈的push序列,判断另一个序列有没有可能是push序列对应的pop序列。
      将push序列元素依次压进栈;同时比较栈顶元素是否与pop序列的第一个元素相同,是则出栈,再判断pop序列的下一个元素;若不相同或栈已空,则继续将下一个push序列元素进栈;重复以上操作,直到push序列都已进栈完毕。然后判断此时的栈是否为空即可。
    4. 用栈实现队列
      使用两个栈(设为 stackA 和 stackB )来实现队列,stackA负责入栈操作,stackB负责出栈。
      入栈:若stackA未满,则入栈 stackA.push;
      若stackA已满,则判断stackB是否是空的,如果是空的,则将stackA的元素出栈并入栈到stackB,再将元素入栈stackA;如果stackB非空,则报错提示 “栈(队列?)已满” ,或使用更多的栈来负责出栈。。。
      出栈:若stackB为空,则将stackA中的元素全部压入stackB,然后在出栈stackB栈顶元素。
      若stackB非空,则直接出栈。

队列

  • 特点:先进先出(FIFO)

  • 应用场景:

  • 例题:

    1. 滑动窗口最大值(最小值同理可得),转自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。
      • 建立两个数组leftright,长度为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]);
      
    2. 用队列实现栈
      一、 使用一个队列来实现

      1. 入栈:直接入队即可;
      2. 出栈:假设此时队列中现有元素个数为n,则依次将队列中的n-1个元素出队再入队,完成后,队尾元素就变成了队头元素,然后出队即可。
        二、 使用两个队列来实现
      3. 入栈:判断两个队列是否为空,把元素放进非空的队列中(空队列用来进行出栈操作);若全为空队列,则随便放进一个队列中;
      4. 出栈:将非空队列中的 n-1 个元素依次出队,并入队到另一个队列(空的)中,然后将第 n 个元素出队即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值