首页 > 其他分享 >计算机基础知识问答-数据结构篇

计算机基础知识问答-数据结构篇

时间:2024-02-25 21:56:10浏览次数:26  
标签:数据结构 复杂度 元素 基础知识 链表 算法 排序 问答 节点

  1. 阐述栈与队列的相同点和不同点

相同点:

  • 栈和队列都是线性数据结构,用于存储数据的集合。
  • 在栈和队列中,数据的插入和删除操作都遵循特定的规则。

不同点:

  • 插入与删除操作的位置:栈是后进先出(LIFO, Last In First Out)的数据结构,只允许在栈顶进行插入(push)和删除(pop)操作。队列是先进先出(FIFO, First In First Out)的数据结构,只允许在队尾进行插入(enqueue)操作,在队头进行删除(dequeue)操作。
  • 操作限制:栈的操作相对受限,只能在栈顶进行。队列则允许在两端进行操作,但遵循FIFO原则。

  1. 如何用栈模拟队列?如何用队列模拟栈?请各自给出C++代码
void useStackAsQueue(Stack* s1, Stack* s2) {  
    /*这里使用push初始化栈区*/
    while (!isEmpty(s2)) {  // 出队操作  
        pop(s2); // 出s2  
    }   
    while (!isEmpty(s1)) {  
        pop(s1); // s1转移到s2  
        push(s2, s1->data[s1->top]);  
    }    //保持入队顺序  
    while (!isEmpty(s2)) {  
        x = pop(s2); // 出队操作  
        printf("%d ", x); 
    }  
    printf("\n");  
}

// 假设我们已经有了一个队列queue,我们将使用它来模拟栈的行为  
// 入栈操作(使用队列的enqueue函数)  
void push(Queue* queue, int value) {  
    enqueue(queue, value);  
}  
// 出栈操作(使用队列的dequeue和enqueue函数模拟)  
int pop(Queue* queue) {  
    // 如果队列为空,则栈也为空,无法出栈  
    if (isEmpty(queue)) {  
        printf("Stack is empty\n");  
        return -1; // 返回一个错误码表示栈空  
    }  
    // 当队列中只有一个元素时,直接出队并返回该元素即可  
    if (queue->front == queue->rear) {  
        int value = dequeue(queue);  
        return value;  
    }  
    // 当队列中有多个元素时,将除了最后一个元素以外的所有元素出队并再次入队  
    while (queue->front != queue->rear - 1) {  
        int temp = dequeue(queue);  
        enqueue(queue, temp);  
    }  
    // 此时队列的最后一个元素即为栈顶元素,出队并返回  
    int value = dequeue(queue);          
    return value;  
}  

  1. 简述二叉树、二叉排序树、完全二叉树、平衡二叉树、大根堆的定义
  • 二叉树:每个节点最多有两个子节点(左子节点和右子节点)的树结构。
  • 二叉排序树(BST, Binary Search Tree):左子树上的所有节点值都小于根节点的值,右子树上的所有节点值都大于根节点的值。
  • 完全二叉树:除了最底层之外,其他层的节点数都达到最大,且最底层节点尽可能集中在左侧的二叉树。
  • 平衡二叉树(AVL树):任意节点的左右子树的高度差不超过1的二叉树。
  • 大根堆(Max Heap):完全二叉树的一种,每个节点的值都大于或等于其子节点的值。

  1. 树的遍历有哪些方法
  • 前序遍历:先访问根节点,然后遍历左子树,最后遍历右子树。
  • 中序遍历:先遍历左子树,然后访问根节点,最后遍历右子树。对于二叉排序树,中序遍历的结果是升序的。
  • 后序遍历:先遍历左子树,然后遍历右子树,最后访问根节点。
  • 层次遍历:按树的层次从上到下、从左到右进行遍历。这通常通过队列来实现。

  1. 深度优先搜索(DFS)和广度优先搜索(BFS)分别都有什么应用?

深度优先搜索(DFS):

  • 图遍历与路径查找:DFS常用于遍历或搜索图或树形结构,它可以用来找到从根节点到叶子节点的所有路径。
  • 图的连通性:在无向图中,DFS可以用来检测两个节点是否连通,或者一个节点是否可达。
  • 检测环:在图中,DFS可以用于检测是否存在环。
  • 回溯算法:DFS经常与回溯算法结合使用,解决如八皇后、图的着色、组合优化等问题。

广度优先搜索(BFS):

  • 最短路径问题:BFS常用于找到图中两个节点之间的最短路径,例如Dijkstra算法和Bellman-Ford算法都使用BFS的思想。
  • 层次遍历:BFS自然地适用于需要按层次或级别遍历的问题,如树的层次遍历或图的层次遍历。
  • 图的连通性:在有向图中,BFS可以用来检测节点是否可达。
  • 广度优先搜索策略:在需要优先探索离起点较近节点的场景下,BFS是一种有效的策略,例如在游戏AI的路径规划中。

这两种搜索策略在不同的应用场景下各有优势,DFS更适合于深度优先的问题,而BFS更适合于广度优先或需要最短路径的问题。


  1. 如何在一棵树插入或删除节点后作调整使其变为平衡二叉树

在平衡二叉树(如AVL树或红黑树)中,插入或删除节点后,可能会导致树失去平衡。为了维持平衡,需要进行适当的调整。以AVL树为例,插入或删除节点后需要进行以下步骤:

  • 检测平衡因子:每个节点都有一个平衡因子,定义为左子树高度减去右子树高度。在插入或删除节点后,从受影响的节点开始,沿着树向上遍历,更新每个节点的平衡因子。
  • 旋转操作:如果某个节点的平衡因子绝对值大于1,说明树失去了平衡。此时,需要进行旋转操作来恢复平衡。
  • 递归调整:旋转操作可能会影响到树的更高层,因此可能需要递归地调整整棵树,直到整棵树重新达到平衡。

平衡二叉树的四种旋转方式:LL,LR,RL,RR


  1. 简述红黑树的基本定义与作用,与AVL树比起来它有什么特点吗

基本定义:红黑树是一种自平衡的二叉查找树,其中每个节点都有一个颜色属性,可以是红色或黑色。红黑树满足以下性质:

  • 每个节点或是红色,或是黑色。
  • 根节点是黑色。
  • 每个叶节点(NIL节点,空节点)是黑色。
  • 如果一个节点是红色的,那么它的两个子节点都是黑色。
  • 对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数量的黑色节点。

作用:红黑树主要用于实现关联数组、有序集合等数据结构,具有高效的插入、删除和查找操作。

与AVL树相比的特点:

  • 平衡性:AVL树要求每个节点的左右子树高度差不超过1,而红黑树只要求黑色节点的高度差不超过2。
  • 旋转操作:AVL树在插入或删除节点后需要进行旋转操作来恢复平衡,而红黑树在插入或删除节点后可能需要进行颜色调整和旋转操作。红黑树的旋转操作相对较少,因为其平衡性条件相对宽松。
  • 性能:红黑树和AVL树在最坏情况下的时间复杂度都是O(log n),但在实际应用中,红黑树由于其较少的旋转操作,通常具有更好的性能。

  1. 常见的稳定排序和不稳定排序都有哪些,它们的时间复杂度和空间复杂度为多少

稳定排序:

  • 冒泡排序:时间复杂度\(O(n^2)\),空间复杂度\(O(1)\)。
  • 插入排序:时间复杂度\(O(n^2)\)(最坏情况),\(O(n)\)(最好情况),空间复杂度\(O(1)\)。
  • 归并排序:时间复杂度\(O(n \log n)\),空间复杂度\(O(n)\)。
  • 计数排序:时间复杂度\(O(n+k)\)(其中k是整数的范围),空间复杂度\(O(k)\)。
  • 基数排序:时间复杂度\(O(d(n+k))\)(其中d是最大数字的位数,k是每位的范围),空间复杂度\(O(n+k)\)。

不稳定排序:

  • 选择排序:时间复杂度\(O(n^2)\),空间复杂度\(O(1)\)。
  • 快速排序:时间复杂度\(O(n \log n)\)(平均情况),\(O(n^2)\)(最坏情况),空间复杂度\(O(\log n)\)(递归调用栈)。
  • 堆排序:时间复杂度\(O(n log n)\),空间复杂度\(O(1)\)。

  1. 图的存储有哪些方法
  • 邻接矩阵:使用一个二维数组来表示图中节点之间的连接关系。适用于稠密图。
  • 邻接表:使用链表或数组来表示每个节点的邻居。适用于稀疏图。
  • 边列表:存储图中所有的边,每个边用一个元组或数组表示。适用于稀疏图。
  • 邻接多重表:结合邻接表和边列表,同时存储节点和边的信息。适用于需要同时访问节点和边的场景。
  • 十字链表:在十字链表中,每个顶点都有两个链表与之相关联:一个是以该顶点为弧尾的弧节点链表,另一个是以该顶点为弧头的弧节点链表。通过遍历这两个链表,我们可以轻松地找到与该顶点相关联的所有弧。

  1. 简述并查集及其应用

并查集是一种用于处理一些不交集(Disjoint Sets)合并及查询问题的数据结构。它支持两种主要操作:联合(Union)和查找(Find)。在并查集中,每个元素都有一个“父”元素,除了一个集合的代表元素(或称为根元素),它的父元素指向自己。


并查集通常使用一个数组parent[]来实现,其中parent[i]表示元素i的父元素。初始时,每个元素的父元素都是它自身,表示每个元素都是独立的集合。


  • 查找(Find):查找操作用于确定两个元素是否属于同一个集合。这可以通过查找两个元素的根元素是否相同来实现。在查找过程中,通常会进行路径压缩,即将查找路径上的所有元素的父元素直接指向根元素,以减少后续查找的时间复杂度。

  • 联合(Union):联合操作用于将两个集合合并为一个集合。这可以通过将其中一个集合的根元素的父元素设置为另一个集合的根元素来实现。同样地,在联合过程中,可以进行按秩合并(或按大小合并),即将较小的树合并到较大的树中,以保持树的平衡性,减少查找操作的时间复杂度。


应用:

  • 图的连通性问题
  • 最小生成树问题
  • 集合查询与存储问题
  • ……

  1. 简述最小生成树算法
  • Kruskal算法:基于边的选择,按边的权值从小到大排序,每次选择权值最小的边,如果这条边与已选边不构成环,则将其加入最小生成树中。
  • Prim算法:基于点的选择,从某一点开始,每次选择与该点相连且权值最小的边,并将该边的另一端点加入生成树中,重复此过程直到所有点都被包含。

  1. 简述最短路径算法
  • Dijkstra算法:适用于没有负权边的图。算法的基本思想是每次从未确定最短路径的顶点中选择一个距离最短的顶点,然后更新其相邻顶点的最短路径。
  • Bellman-Ford算法:适用于含有负权边的图。算法的基本思想是通过不断松弛边的权值来逐步逼近最短路径,最终得到源点到所有其他顶点的最短路径。
  • Floyd算法是一种动态规划算法,其基本思想是通过逐步累加中间点来逐步得到所有顶点对之间的最短路径。

  1. 你知道外部排序算法吗?请简述一下选择归并排序的思想

外部排序算法用于处理数据量过大,无法一次性加载到内存中进行排序的情况。选择归并排序是一种常用的外部排序算法。

选择归并排序的基本思想是将大文件划分为多个小文件,对每个小文件分别进行排序,然后将这些有序的小文件两两归并,得到更大的有序文件,重复此过程直到所有文件归并为一个有序的大文件。


  1. 如果现在有4M条数据需要排序,但内存仅支持1M条数据的存储,这时候该如何处理

方法一:分块排序与多路归并

  • 分块:将4M条数据分为4个块,每个块大约1M条数据。

  • 排序:对每个块内的数据进行排序。由于每个块只有1M条数据,这可以在内存中完成。

  • 多路归并:

    • 创建一个最小堆(或优先队列),其中每个元素是一个(块索引, 块内索引)对,表示当前正在考虑的块的当前元素的索引。
    • 初始化堆,为每个块添加其第一个元素。
    • 从堆中弹出最小元素,并输出它。
    • 为弹出元素的块添加下一个元素(如果存在)。
    • 重复此过程,直到所有块中的所有元素都被处理。

方法二:外部排序与缓冲

  • 分块:同样将4M条数据分为4个块。

  • 排序与输出:对每个块进行排序,直接写入输出文件。

  • 合并:使用k路归并来合并这些已排序的块。这可以通过使用一个大小为k的窗口来实现,其中k是你可以同时加载到内存中的块数。

    • 加载k个块到内存中。
    • 对这些块中的第一个元素进行比较,选择最小的元素写入输出文件,并移动该元素的指针。
    • 如果某个块的所有元素都被写入,则从内存中移除该块,并加载下一个未排序的块。
    • 重复此过程,直到所有块都被处理。

方法三:使用外部排序工具

在许多情况下,你不需要自己实现整个外部排序算法。许多编程语言和工具库都提供了外部排序的功能。例如,在Unix/Linux系统中,你可以使用sort命令的-o和-T选项来指定输出文件和临时文件的目录。这样,sort命令会自动处理数据的分块、排序和合并。

优化

  • 减少磁盘I/O:通过增加缓冲区大小、使用更有效的磁盘访问模式(如顺序访问而不是随机访问)来减少磁盘I/O操作。
  • 并行处理:如果可能,使用多个线程或进程并行地对不同的块进行排序和归并,以加快处理速度。

  1. 在树和图的有关算法中,基于递归和基于非递归的算法有怎样的特性

基于递归的算法通常具有简洁明了的代码结构,易于理解和实现。递归算法通过函数调用自身来解决问题,通常适用于具有明显递归结构的问题,如树的遍历、图的搜索等。然而,递归算法可能会占用较多的栈空间,并且在处理大数据量时可能导致栈溢出。

基于非递归的算法通常使用循环和显式的数据结构(如栈、队列等)来解决问题。非递归算法具有较低的空间复杂度,可以避免栈溢出的问题。此外,非递归算法通常具有较好的可优化性和可扩展性,可以通过调整循环条件和数据结构来提高算法性能。然而,非递归算法的代码结构可能较为复杂,需要更多的编程技巧和思维。


  1. 请推导快速排序的时间复杂度

\[T(n)=2T(\frac{n}{2})+O(n) \]


  1. 你了解HashMap的底层逻辑吗
import java.util.HashMap;  //HashMap在Java中是一个基于哈希表的Map接口的实现。
public class HashMapExample {  
    public static void main(String[] args) {  
        // 创建一个新的 HashMap  
        HashMap<String, Integer> map = new HashMap<>();  
        // 向 HashMap 中添加键值对  
        map.put("apple", 1);  
        // 从 HashMap 中获取值  
        int appleCount = map.get("apple");  
        System.out.println("Number of apples: " + appleCount);  
        // 检查 HashMap 中是否包含某个键  
        if (map.containsKey("banana")) {  
            System.out.println("Banana is in the map.");  
        }  
        // 遍历 HashMap  
        for (String fruit : map.keySet()) {  
            System.out.println("Fruit: " + fruit + ", Count: " + map.get(fruit));  
        } 
        // 删除 HashMap 中的键值对  
        map.remove("cherry");  
        System.out.println("Cherry has been removed.");  
    }  
}

其底层逻辑主要包括以下几个部分:

  • 数组:HashMap使用一个数组来存储键值对。数组的每个元素是一个链表(在Java 8及之后的版本中,链表可能会被红黑树替代,当链表长度超过一定阈值时)。
  • 哈希函数:HashMap使用一个哈希函数来计算键的哈希值,从而确定键值对在数组中的位置。
  • 碰撞解决:当两个键的哈希值相同时(即它们映射到数组的同一个位置),HashMap使用链表(或红黑树)来存储这些键值对,形成所谓的“桶”(bucket)。
  • 扩容:当HashMap中的元素数量超过数组大小的某个阈值时,HashMap会创建一个新的数组,并重新计算所有键值对的哈希值,将它们放入新的数组中。这个过程称为“rehashing”。

  1. 哈希表出现冲突时可以如何解决冲突

哈希表出现冲突时,可以采用以下几种常见的冲突解决方法:

  • 开放寻址法:当哈希值对应的桶位置已经被占用时,按照一定的顺序(如线性探测、二次探测、双重哈希等)在桶中查找下一个可用的位置。
  • 链地址法:每个桶位置都维护一个链表(或红黑树等数据结构),当哈希值对应的桶位置已经被占用时,将新元素添加到链表的末尾(或红黑树中)。
  • 再哈希法:当哈希值对应的桶位置已经被占用时,使用另一个哈希函数计算新的哈希值,直到找到一个可用的位置。

  1. 数据结构中的一些算法用面向对象的形式实现相比面向过程而言有哪些优势
  • 代码可重用性:面向对象的设计使得代码更易于重用。通过封装和继承,可以创建可重用的类和对象,减少代码冗余。
  • 代码可维护性:面向对象的设计使得代码更易于理解和维护。通过将数据和操作封装在对象中,可以提高代码的内聚性,降低耦合性,从而简化代码的修改和维护。
  • 扩展性:面向对象的设计支持通过继承和多态实现代码的扩展。这使得在不修改原有代码的情况下,可以通过添加新的类或对象来实现新的功能。
  • 模拟现实世界:面向对象的设计更接近现实世界的模型。通过将现实世界中的实体抽象为对象,可以更好地模拟和解决实际问题。

  1. 数据库系统为什么钟爱用B+树作索引?B树与B+树相比异同点在哪里

一个好的数据库索引应该具备以下特性:

  • 快速查询:索引应该能够快速定位到需要的数据,减少全表扫描的次数。
  • 高效插入和删除:随着数据的更新(插入和删除),索引结构应该能够高效地调整。
  • 磁盘I/O优化:由于数据库数据通常存储在磁盘上,因此索引结构应该尽量减少磁盘I/O操作。
  • 数据稳定性:索引结构应该能够在数据变化时保持相对稳定,避免频繁的重建。
  • 范围查询支持:索引应该能够高效地支持范围查询。

B+树满足了上述的特性和需求,具体来说:

  • 节点分裂与合并:B+树在插入和删除时可以通过节点的分裂与合并来维持平衡,保证了查询效率的稳定。
  • 减少磁盘I/O:B+树的非叶子节点不存储数据,使得每个节点能够存储更多的键值,从而减少树的高度,进而减少了查询时需要的磁盘I/O次数。
  • 范围查询优化:B+树的所有叶子节点都通过指针相连,形成了一个有序的链表结构。这使得范围查询非常高效,因为只需要从链表的起始节点开始遍历到结束节点即可。
  • 数据稳定性:B+树的叶子节点包含了所有的数据,这意味着在数据插入、删除时,只需要调整叶子节点,非叶子节点的调整相对较少,从而保证了数据的稳定性。

B树与B+树的异同点

  • 数据结构:B树每个节点既存储键值也存储数据,而B+树只有叶子节点存储数据,非叶子节点只存储键值和子节点指针。
  • 磁盘I/O:由于B+树非叶子节点不存储数据,节点能够存储更多的键值,因此B+树通常比B树具有更少的树高度,从而减少了磁盘I/O操作。
  • 范围查询:B+树叶子节点之间的有序链表结构使得范围查询更加高效,而B树在范围查询时需要从根节点开始遍历。
  • 数据稳定性:B+树的数据全部存储在叶子节点,非叶子节点的调整相对较少,这使得B+树在数据更新时更加稳定。

  1. 手写一个翻转单链表的C代码
Node* reverseList(Node* head) {  
    Node* prev = NULL;  
    Node* current = head;  
    Node* next = NULL;  
  
    while (current != NULL) {  
        next = current->next;  
        current->next = prev;  
        prev = current;  
        current = next;  
    }  
    return prev;  
}  

  1. 如何判断一个链表中存在环?给出至少三种算法思想
  • 快慢指针法(Floyd's Cycle-Finding Algorithm):初始化两个指针,一个快指针每次移动两步,一个慢指针每次移动一步。如果链表中存在环,那么快慢指针最终会相遇;如果链表中不存在环,快指针会先到达链表的尾部。

  • 哈希表法:使用一个哈希表来存储访问过的节点。遍历链表,对于每个访问的节点,检查它是否已经在哈希表中。如果是,则说明链表中存在环;否则,将其加入哈希表。如果遍历结束后没有找到环,则说明链表不含有环。

  • 链表长度法:遍历链表,同时计算链表的长度。当遍历到尾部时,比较链表长度与原先记录的长度是否相等。如果长度增加,则说明存在环;如果长度不变,则说明不存在环。这种方法需要遍历两次链表,第一次获取长度,第二次验证长度是否增加。

  1. 手写一个计算二叉树高度的C代码,并基于此判断一个二叉树是否为平衡二叉树
// 计算二叉树高度的函数  
int getHeight(Node* node) {  
    if (node == NULL) {  
        return 0;  
    }  
    int leftHeight = getHeight(node->left);  
    int rightHeight = getHeight(node->right);  
    return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;  
}  

// 判断二叉树是否平衡的函数  
int isBalanced(Node* node) {  
    if (node == NULL) {  
        return 1;  
    }  
    int leftHeight = getHeight(node->left);  
    int rightHeight = getHeight(node->right);  
    if (abs(leftHeight - rightHeight) > 1) {  
        return 0;  
    }  
    return isBalanced(node->left) && isBalanced(node->right);  
}  

  1. 简述字符串模式匹配中的KMP算法

KMP (Knuth-Morris-Pratt) 算法是一种高效的字符串匹配算法,它利用了部分匹配信息来避免在匹配过程中的回溯。KMP算法通过预处理模式串(要查找的字符串),构建一个“最长公共前后缀数组”(LPS数组),该数组记录了模式串中每个子串的最长公共前后缀长度。

在匹配过程中,当发现字符不匹配时,KMP算法会根据LPS数组找到模式串中下一个可能的匹配位置,然后将模式串向右滑动到该位置继续匹配,而不是像朴素匹配算法那样每次只滑动一个字符。通过这种方式,KMP算法能够在一次匹配失败后跳过一些肯定不会匹配的位置,从而提高匹配效率。


计算案例:


  1. C++中使用String类实现字符串和使用char []实现字符串、char *实现字符串有何异同

内存管理:

  • std::string:自动管理内存,会根据需要动态分配和释放内存。
  • char []:在栈上分配固定大小的内存,大小在编译时确定。
  • char *:通常指向堆上分配的内存或通过其他方式获得的内存块,需要手动管理内存。

易用性和安全性:

  • std::string:提供了丰富的成员函数和操作符重载,易于使用且较为安全(例如,不容易发生缓冲区溢出)。
  • char []:需要手动处理字符串的复制、连接等操作,且容易发生缓冲区溢出等问题。
  • char *:同样需要手动处理字符串操作,且指针操作更容易出错(如野指针、空指针解引用等)。

性能:

  • 对于小字符串和简单操作,char []和char *可能由于直接内存访问而具有更好的性能。
  • 对于大字符串和复杂操作,std::string的封装和内部优化通常能提供足够的性能,同时保持代码的清晰和可维护性。

兼容性:

  • char []和char *与C语言兼容,适用于需要与C语言库交互的场景。
  • std::string是C++特有的,提供了更多面向对象的特性。

生命周期:

  • std::string和char []的生命周期通常与它们的作用域绑定。
  • char *的生命周期取决于它所指向的内存何时被释放。如果它指向的是动态分配的内存,那么需要手动释放这块内存来避免内存泄漏。

  1. 稀疏矩阵如何压缩存储

常见的稀疏矩阵压缩存储方法有:

  • 三元组表:对于非零元素,存储其行号、列号和值。这种方法适用于非零元素分布较均匀的情况。
  • 十字链表:每个非零元素存储在一个节点中,节点包含行号、列号、值和两个指针,分别指向同行和同列的下一个非零元素。这种方法便于进行矩阵的转置操作。

  1. 给出拓扑排序的C语言代码,其中图以邻接矩阵的形式存储
#include <stdio.h>  
#include <stdlib.h>  
#define MAX_VERTICES 100  
int graph[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵  
int indegree[MAX_VERTICES]; // 记录每个顶点的入度  
int queue[MAX_VERTICES]; // 用于拓扑排序的队列  
int front = 0, rear = 0; // 队列的队首和队尾指针  
  
void topologicalSort(int vertices) {  
    int i, j, k;   
    /* 初始化入度数组  */
    /* 将所有入度为0的顶点入队  */
    // 拓扑排序  
    while (front < rear) {  
        int current = queue[front++];  
        printf("%d ", current); // 输出当前顶点  
          
        // 减少与当前顶点相邻的顶点的入度,并将入度减为0的顶点入队  
        for (j = 0; j < vertices; j++) {  
            if (graph[current][j] == 1) {  
                indegree[j]--;  
                if (indegree[j] == 0) {  
                    queue[rear++] = j;  
                }  
            }  
        }  
    }  
}  

int main() {  
    int vertices, edges, i, j;   
    // 读取顶点数和边数  
    printf("Enter number of vertices and edges: ");  
    scanf("%d %d", &vertices, &edges);    
    // 初始化邻接矩阵  
    for (i = 0; i < vertices; i++) {  
        for (j = 0; j < vertices; j++) {  
            graph[i][j] = 0;  
        }  
    }   
    // 读取边的信息  
    printf("Enter edges (from to): \n");  
    for (i = 0; i < edges; i++) {  
        int from, to;  
        scanf("%d %d", &from, &to);  
        graph[from][to] = 1; // 设置邻接矩阵  
    }   
    // 进行拓扑排序  
    topologicalSort(vertices);  
    return 0;  
}

  1. 如果实现一个航空订票系统,你会如何设计各个对象的数据结构与基本操作

设计思路:


  1. 简述Set

在Java中,Set是一个接口,它属于java.util包。Set接口代表一个集合,它不允许包含重复的元素。与List不同,Set中的元素是无序的,即元素没有索引,也不能通过索引来访问元素。


Java中Set接口的主要实现类有:

  • HashSet:基于哈希表实现的Set接口,它提供了快速的元素添加、删除和查找操作。由于使用了哈希表,HashSet中的元素是无序的。
  • TreeSet:基于红黑树实现的Set接口,它按照元素的自然顺序或者创建TreeSet时提供的Comparator来排序元素。TreeSet不允许插入null元素(除非指定了自定义的比较器)。
  • LinkedHashSet:这是一个有序的Set实现,它使用链接列表来维护元素的插入顺序。在迭代时,LinkedHashSet会按照元素插入的顺序返回元素。

Set接口定义了一些重要的方法,如:

  • add(E e): 将指定的元素添加到集合中(如果尚未存在)。
  • remove(Object o): 如果集合包含指定的元素,则将其移除。
  • contains(Object o): 如果集合包含指定的元素,则返回true。
  • isEmpty(): 如果集合不包含任何元素,则返回true。
  • size(): 返回集合中的元素数量。
  • iterator(): 返回一个迭代器,用于遍历集合中的元素。

import java.util.HashSet;  
import java.util.Set;  
public class SetExample {  
    public static void main(String[] args) {  
        Set<String> mySet = new HashSet<>();  
        // 添加元素  
        mySet.add("apple");  
        mySet.add("banana");  
        mySet.add("orange");  
        mySet.add("apple"); // 这个不会被添加,因为Set不允许重复元素  
        // 输出集合中的元素  
        for (String fruit : mySet) {  
            System.out.println(fruit);  
        }  
        // 检查元素是否存在  
        System.out.println("Contains apple? " + mySet.contains("apple"));  
        // 移除元素  
        mySet.remove("banana");  
        System.out.println("Contains banana now? " + mySet.contains("banana"));  
        // 输出集合大小  
        System.out.println("Size of the set: " + mySet.size());  
    }  
}

  1. 你通常会使用哪些手段优化你的时间复杂度或空间复杂度

时间复杂度优化:

  • 算法选择:选择时间复杂度较低的算法。例如,如果需要在列表中查找元素,使用哈希表(O(1))而不是线性搜索(O(n))。
  • 避免嵌套循环:尽量减少嵌套循环的使用,因为它们会显著增加时间复杂度。
  • 动态规划:使用动态规划技术来存储子问题的解,从而避免重复计算。
  • 分治策略:将大问题分解为更小、更易于解决的子问题,然后合并子问题的解来得到原问题的解。

  • 剪枝:在搜索算法中,通过提前排除不可能得到最优解的分支来减少搜索空间。
  • 缓存:将计算结果缓存起来,以便后续可以直接使用而不需要重新计算。
  • 并行计算:利用多核处理器并行处理任务,从而加速计算过程。

空间复杂度优化:

  • 避免不必要的数据结构:选择更紧凑的数据结构或避免创建不必要的数据结构。
  • 原地算法:使用原地算法,即不使用额外空间或仅使用固定额外空间的算法。
  • 迭代代替递归:递归可能会消耗大量栈空间,可以考虑使用迭代来替代。

  • 数据共享:在可能的情况下,让多个变量或数据结构共享同一块内存空间。
  • 压缩数据:如果数据可以压缩,使用压缩技术来减少存储需求。
  • 内存管理:合理使用内存,及时释放不再使用的内存资源。
  • 数据结构优化:选择更适合问题需求的数据结构,例如使用位运算代替整数,使用指针代替对象等。

标签:数据结构,复杂度,元素,基础知识,链表,算法,排序,问答,节点
From: https://www.cnblogs.com/Mast1031/p/18033161

相关文章

  • Python数据结构与算法05——二分查找
    二分查找——递归版:defbinarySearch(aimlist,item):#获取列表的长度n=len(aimlist)#如果列表非空ifn>0:#计算中间索引mid=n//2#如果中间元素是目标元素,则找到了ifaimlist[mid]==item:......
  • 前端树形Tree数据结构使用-‍♂️各种姿势总结
    01、树形结构数据前端开发中会经常用到树形结构数据,如多级菜单、商品的多级分类等。数据库的设计和存储都是扁平结构,就会用到各种Tree树结构的转换操作,本文就尝试全面总结一下。如下示例数据,关键字段id为唯一标识,pid为父级id,用来标识父级节点,实现任意多级树形结构。"pid":0“......
  • Python数据结构与算法05——归并排序
    归并排序:defmerge_sort(aimlist):#归并排序拆分-排序-合并也就是merge_返回的是是一个已经排好序的列表n=len(aimlist)ifn<=1:returnaimlistmid=n//2aimlist_left=merge_sort(aimlist[:mid])aimlist_right=merge_sort(aimlist[mid:......
  • 数据结构可视化网站(B-tree & B+tree)
    -网址:B-TreeVisualizationB+TreeVisualization(usfca.edu)  ......
  • 【数据结构】C语言实现图的相关操作
    图图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。术语无向图:每条边都是无方向的图有向图:每条边都是有方向的图完全图:任意两个点都有一条边相连的图边:无向图中的边弧:有向图中的边稀疏......
  • C#数据结构 HashSet 用法
    所属命名空间.NET3.5在System.Collections.Generic命名空间中包含一个新的集合类:HashSet这个集合类包含不重复项的无序列表称作HashSet。(类似C++的unordered_set?)这个集合基于散列hash值,插入元素的操作非常快,不需要像List类那样重排集合。操作函数表Add重载:Hashset还提......
  • C#数据结构 字典Dictionary
    简介字典是C#开发中经常使用的一种键值对容器,类似C++的map,可使用foreach或迭代器遍历不能装多个相同key,底层实现是哈希函数具体用法1.创建Dictionary<key,value>//Key和Value可以是任意类型Dictionary<int,string>_testDic=newDictionary<int,string>();2.添加......
  • UOJ228/HDU5828 基础数据结构练习题/Rikka with Sequence 题解(势能线段树)
    势能线段树。如果线段树上一个节点的\(\max-\min\ge2\),我们称其为关键节点,考虑定义势能\(\phi\)为线段树上关键节点的个数。对于每次开方操作,如果当前节点为关键节点,则暴力递归左右儿子修改,否则:如果当前节点\(\max=\min\)或\(\max=\min+1\)且\(\max\)不是完全平方数,......
  • 【学习笔记】 - 基础数据结构 :Link-Cut Tree
    发现树剖代码太长了,给我恶心坏了学个代码短点的能写树剖题的数据结构吧前置知识平衡树splay树链剖分简介以及优缺点介绍Link-CutTree,也就是LCT,一般用于解决动态树问题Link-CutTree可用于实现重链剖分的绝大多数问题,复杂度为\(O(n\logn)\),看起来比树剖的\(O(n\lo......
  • 数据结构——线性表
    线性表1.掌握线性表的定义、逻辑结构及其抽象数据类型等基本概念线性表的抽象数据类型定义ADTList{数据对象:D={ai|ai∈ElemSet,i=1,2,...n,n>=0}//任意数据元素的集合数据关系:R={<ai-1,ai>|ai-1,ai∈D,i=2,3,...n}//除第一个和最后一个外,每个元素都有唯一的前驱和后继基本......