数据结构

名词解释

  1. 逆波兰式:

    后缀表达式, 运算符在操作数后面.

  2. 自由树:

    无回路的连通图.

  3. 外部排序:

    将待排序的记录存储在外存上, 排序时再把数据一部分一部分地调入内存进行排序. 在排序过程中需要多次进行外存和内存之间的交换, 对外存文件中的记录进行的结果仍然放到原有文件中.

  4. 邻接表:

    对图 G 中的每个顶点 V1 建立一个单链表, 第 i 个单链表中的结点表示依附于顶点 Vi 的边. 边表的头指针和顶点的数据信息采用顺序存储.

  5. 占位程序:

    缺失或未经测试的函数的简化版, 用于产生足以用于测试的值.

队列的实现方法有哪些? 试比较各种实现方法的优缺点, 并距离说明队列在计算机系统中有何应用?

  1. 队列的实现方法有:
    1. 顺序队列, 即采用顺序存储结构存储队列.
    2. 链式队列, 即采用链式存储结构存储队列.
  2. 各有什么优缺点:
    1. 顺序队列
      1. 优点: 可以一次性分配保证够用的空间, 效率高, 因为是基于数组的, 长度也是固定的. 可以实现动态容量.
      2. 缺点: 动态分配长度时, 效率低下.
    2. 链表队列
      1. 优点: 可以方便快速地动态增长.
      2. 缺点: 由于基于链表, 要动态创建和删除节点, 效率较低.
  3. 应用
    1. 解决主机于外部设备之间的速度不匹配的问题.
    2. 解决由多用户引起的资源竞争问题.

链表的交集

def intersection(l1: ListNode, l2: ListNode) -> ListNode:
    """
    求两个有序单链表的交集, 存放在l1中.
    设置两个指针分别指向两个链表的开头, 如果两个指针指向的结点的值相同, 则表示位于交集内.
    如果不相同, 则比较两个结点的值的大小, 如果l1的大, 则移动l2的指针. 否则删除当前l1的结点.

    Args:
        l1 (ListNode): 有序单链表1
        l2 (ListNode): 有序单链表2

    Returns:
        ListNode: 交集的单链表
    """
    if not l1 or not l2:
        return l1
    h1, h2 = ListNode(0, l1), ListNode(0, l2)
    t1, t2 = h1, h2
    while t1.next and t2.next:
        # 如果相等则t1,t2移动到下一格
        if t1.next.val == t2.next.val:
            t1, t2 = t1.next, t2.next
        # 如果大于则移动t2, 直到相等或者大于t1.val
        elif t1.next.val > t2.next.val:
            t2 = t2.next
        # 如果t1.val < t2.val 则移动t1, 且跳过当前结点
        else:
            t1.next = t1.next.next
    return l1

合并二叉排序树

def insert_node(tree: TreeNode, target: TreeNode):
    """
    将一个节点插入到二叉排序树中.

    Args:
        tree (TreeNode): 二叉排序树
        target (TreeNode): 目标节点
    """
    if target.val == tree.val:
        return
    elif target.val < tree.val:
        if tree.left:
            insert_node(tree.left, target)
        else:
            tree.left = target
    else:
        if tree.right:
            insert_node(tree.right, target)
        else:
            tree.right = target


def merge_search_tree(tree: TreeNode, target: TreeNode) -> TreeNode:
    """
    合并两个二叉排序树.

    后序遍历第二个二叉排序树, 对于遍历到的每个节点, 将其作为单个节点插入到第一个二叉排序树中,
    然后返回第一个二叉排序树.

    Args:
        tree (TreeNode): 第一个二叉排序树
        target (TreeNode): 二个二叉排序树

    Returns:
        TreeNode: 合并后的二叉排序树
    """

    def post_order(idx: TreeNode):
        """
        后序遍历

        Args:
            idx (TreeNode): 遍历的当前节点
        """
        if idx.left:
            post_order(idx.left)
        if idx.right:
            post_order(idx.right)
        idx.left, idx.right = None, None
        insert_node(tree, idx)

    post_order(target)
    return tree

有向无环图的最长路径

def longest_path(_map: dict, start: int = 0):
    """
    求DAG的单源最长路径.

    可以转换成求所有权值为相反数的最短路径. 最短路径可以使用Dijkstra求出.

    Args:
        _map (dict): DAG
    """
    length = [float('inf') for _ in range(len(_map))]
    pre = [start for _ in range(len(_map))]
    que = [[-v[0], v[1], v[2]] for v in _map[start]]
    heapq.heapify(que)

    for _ in range(len(_map) - 1):
        idx_len, idx_node, idx_pre = heapq.heappop(que)
        length[idx_node] = idx_len
        pre[idx_node] = idx_pre
        for v in _map[idx_node]:
            heapq.heappush(que, [-v[0] + idx_len, v[1], v[2]])

    max_node = start
    for i in range(len(length)):
        if length[i] < length[max_node]:
            max_node = i

    print(f'The longest path length is {-length[max_node]}')

    path = [str(max_node)]
    while pre[max_node] != max_node:
        max_node = pre[max_node]
        path.append(str(max_node))
    path_str = '->'.join(path[::-1])
    print(f'The path is {path_str}')

由于使用到了最小堆, 并且每条边都会进行加入最小堆操作, 因此时间复杂度为 O(E+V*log(V))

操作系统

请判断下属说法的对错, 并说明原因

  1. 分时操作系统必然建立在多道程序技术的基础之上

    错误, 多道程序设计引入的目的是提高 CPU 的使用率, 两者无关.

  2. 进程是指令的集合

    错误, 进程是任务的执行者, 程序是机器指令和数据的集合, 进程可以被看作正在运行的计算机程序.

  3. 存储保护的功能是限制内存存取

    错误, 保证进入内存的各道作业都在自己的存储空间内运行, 互补干扰.

  4. 位示图可用于主存空间的共享

    错误, 用于管理磁盘空间和主存空间.

内存管理(非连续)

连续分配、链接分配、UNIX inode 分配. 详细说明下列的文件访问需求, 采用那种分配方案最合适.

大文件顺序访问

连续分配, 因为每个文件占据磁盘上的一组连续的块, 适合文件顺序访问.

大文件直接访问

UNIX inode, 可以将所有的数据块指针集中到索引块中, 方便直接访问文件.

小文件直接访问

链接分配: 可以直接访问文件, 适合小文件直接访问.

[连续分配] :

  • 每个文件在磁盘上占有一组连续的块。
  • 优点: 实现简单, 用于连续分配文件的所需寻道数量最小, 在确实需要寻道时所需的寻道时间也最小.
  • 缺点: 为新文件找到足够可分配空间较为困难, 会存在外部碎片.

[链接分配] :

  • 采用链接分配, 每个文件是磁盘块的链表, 一个链表包含一个文件的内容.
  • 优点: 解决了连续分配的分配文件困难的问题, 因为可以随机散布一个文件内容, 所以不要求文件保存的连续性.
  • 缺点: 仅能有效用于顺序访问文件, 要找到文件的 i 块, 就必须冲文件开始起, 跟着指针一直到第 i 块. 不能有效地支持文件直接访问. 在空间利用上, 指针也占用了额外的空间.

[索引分配(UNIX inode)] :

  • 索引分配在链接分配基础上, 将所有指针放在一起, 依次排列, 形成索引块. 解决了链接分配不能直接访问的问题.
  • 优点: 既没有外部碎片, 让文件可以在磁盘上随机存储, 又解决了直接访问的问题.
  • 缺点: 浪费空间, 每个文件都需要有一个索引块, 而当文件过小时, 也需要分配一个完整的索引块. 而索引块的大小也有不同的机制进行管理.

什么是虚拟设备, 为什么在操作系统中要引入虚拟设备?

将一台物理 I/O 设备虚拟为多台逻辑上的 I/O 设备, 并允许每个用户占用一台逻辑上的 I/O 设备.

这样可以使原来仅允许在一段时间内由一个用户访问的设备(临界资源), 变为在一段时间内允许多个用户同时访问的共享设备.

哲学家就餐问题

概述: 每个哲学家会优先去去拿序号低的一侧的筷子, 然后去拿序号高的一侧的筷子. 不去拿中间的筷子.

// 表示哲学家周围的筷子资源, 初始都为 1, 表示都未被占用.
Semaphore chopsticks[5] = {1,1,1,1,1};

// 表示编号为 i 的哲学见的行动
Procedure philosopher(int i){
    while(true){
        int priority_chopstick = 0;
        thinking();
        if i != 4{
            P(chopsticks[i]);
            P(chopsticks[i+1]);
            eating();
            V(chopsticks[i+1]);
            V(chopsticks[i]);
        } else {
            P(chopsticks[0]);
            P(chopsticks[4]);
            eating();
            V(chopsticks[0]);
            V(chopsticks[4]);
        }
    }
}

正是你花费在玫瑰上的时间才使得你的玫瑰花珍贵无比