每个开发人员必须知道的顶级数据结构和算法

2025-05-24

每个开发人员必须知道的顶级数据结构和算法

本文由 Aaron Xie 撰写,最初发表于Educative, Inc.

很多人一想到编程面试就心生畏惧。它压力重重,令人精疲力竭,充满挑战。很多时候,光是知道该准备哪些主题就已经让人头疼。不过,今天我们将向你介绍编程面试中会考的主要数据结构和算法。读完这篇文章,你应该会对如何准备才能获得理想的工作有一个清晰的认识。

我们将介绍以下内容:

为什么要学习数据结构和算法?

编程面试考察你的解决问题能力以及对计算机科学概念的理解。通常情况下,你会有大约 30 到 45 分钟的时间来解决一个复杂问题,但有时也会安排几个简单的技术问题。

这时数据结构和算法就派上用场了。这些面试会考察你链表、队列、排序、搜索等诸多主题,所以做好准备至关重要。公司不仅想测试你的技术知识,还想评估你的解决问题的能力。如果你得到了这份工作,你经常会遇到需要修复的错误,而公司希望确保你能够克服这些障碍。此外,你通常会使用特定的数据结构和算法来优化你的代码,使其尽可能高效地运行。

理解大 O 符号

大 O 符号是一种渐近分析,用于描述算法执行所需的时间。换句话说,它用于描述算法的效率或复杂程度。

大 O描述了算法相对于其输入 $N$ 的执行时间,即随着输入的增加而产生的运行时间。虽然我们可以使用平均情况和最佳情况来分析算法的效率,但我们通常使用大 O 符号表示最坏情况。

今天,你将学习时间复杂度,但请注意,空间复杂度也是一个需要理解的重要概念。运行时复杂度一开始可能比较难理解,所以让我们来看一些例子。

$O(1)$

public int findInt(int[] arr) {
    return arr[0];
}
Enter fullscreen mode Exit fullscreen mode

O(1)描述了一种无论输入如何,始终以常数时间运行的算法。无论数组中包含一千个整数还是一个整数,该函数都将同时执行,因为它只需要一步。

$O(N)$

public int func(int[] arr) {
    for (int i = 1; i <= arr.length; i++) {
        System.out.println(i);
    }
}
Enter fullscreen mode Exit fullscreen mode

$O(N)$ 描述的是一种算法,其运行时间相对于输入 $N$ 线性增加。上述函数将对数组中存储的 $N$ 个值执行 $N$ 步。例如,如果数组长度为 1,则该函数运行 1 个“步”;而如果数组长度为 1000,则该函数运行 1000 个“步”。

在提供的示例中,数组长度是输入大小,迭代次数是运行时间。

$O(N^2)$

public int func(int[] arr) {
    for (int i = 1; i <= arr.length; i++) {
        for (int i = 1; i <= arr.length; i++) {
        System.out.println(i);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

$O(N^2)$ 描述的是一个函数,其运行时间与输入大小的平方成正比。当存在一个嵌套循环,使得外层循环运行 $N$ 次,而内层循环在外层循环每次迭代时运行 $N$ 次,使得该函数需要 $N^2$ 步时,就会出现这种运行时间。

需要记住的一些规则

  • 忽略常量:使用大 O 符号时,总是会忽略常量。因此,即使运行时复杂度为 O(2N),我们也称之为 O(N)。

  • 删除次要项:在大 O 时,只需保留最主要的项。例如,$O(N^3 + 50N +1 7)$ 就是 $O(N^3)$。经验法则如下:$O(1)$ < $O(logN)$ < $O(N)$ < $O(NlogN)$ < $O(N^2)$ < $O(2^N)$ < $O(N!)$。

想了解更多关于大 O 符号的知识吗?请查看我们的“什么是大 O 符号”文章。

替代文本

要学习的重要数据结构

简单来说,数据结构是在计算机中组织和存储数据以便访问和修改数据的方式。你将学习编程面试中会考到的重要数据结构的基础知识。

数组

数组是相同变量类型的元素的集合,这些元素按顺序存储在内存中。它是最流行且最简单的数据结构之一,常用于实现其他数据结构。

数组中的每个项都从 0 开始索引,每个项称为一个元素。另外需要注意的是,在 Java 中,数组的大小无法更改。如果需要动态调整大小,建议使用 List。

替代文本

在上面的例子中:

  • 数组的长度为5

  • 索引 3 处的值为 1

  • 在Java中,此数组中的所有值必须是整数类型

在 Java 中初始化数组:

int[] intArray = new int[14];

intArray[3] = 5;
intArray[4] = 3;
intArray[13] = 14;

// Indexes with no value are null
Enter fullscreen mode Exit fullscreen mode

常见面试问题:

  • 查找数组中第一个不重复的整数

  • 按降序重新排列数组

  • 将数组向右旋转一个索引

  • 使用分治法计算子数组的最大和


链接列表

链表是相互链接的节点组成的线性序列。每个节点包含一个值和一个指向列表中下一个节点的指针。与数组不同,链表没有索引,因此必须从第一个节点开始遍历每个节点,直到到达第 n 个节点。最后,最后一个节点将指向空值。

第一个节点称为,最后一个节点称为。下图展示了一个单链表。

替代文本

链表有许多有用的应用:

  • 实现堆栈、队列和哈希表

  • 创建目录

  • 多项式表示和算术运算

  • 动态内存分配

Java中链表的基本实现:


import java.io.*; 

// Java program to implement 
// a Singly Linked List 
public class LinkedList { 

    Node head; // head of list 

    // Linked list Node. 
    // This inner class is made static 
    // so that main() can access it 
    static class Node { 

        int data; 
        Node next; 

        // Constructor 
        Node(int d) 
        { 
            data = d; 
            next = null; 
        } 
    } 

    // Method to insert a new node 
    public static LinkedList insert(LinkedList list, int data) 
    { 
        // Create a new node with given data 
        Node new_node = new Node(data); 
        new_node.next = null; 

        // If the Linked List is empty, 
        // then make the new node as head 
        if (list.head == null) { 
            list.head = new_node; 
        } 
        else { 
            // Else traverse till the last node 
            // and insert the new_node there 
            Node last = list.head; 
            while (last.next != null) { 
                last = last.next; 
            } 

            // Insert the new_node at last node 
            last.next = new_node; 
        } 

        // Return the list by head 
        return list; 
    } 

    // Method to print the LinkedList. 
    public static void printList(LinkedList list) 
    { 
        Node currNode = list.head; 

        System.out.print("LinkedList: "); 

        // Traverse through the LinkedList 
        while (currNode != null) { 
            // Print the data at current node 
            System.out.print(currNode.data + " "); 

            // Go to next node 
            currNode = currNode.next; 
        } 
    } 

    // Driver code 
    public static void main(String[] args) 
    { 
        /* Start with the empty list. */
        LinkedList list = new LinkedList(); 

        // 
        // ******INSERTION****** 
        // 

        // Insert the values 
        list = insert(list, 1); 
        list = insert(list, 2); 
        list = insert(list, 3); 
        list = insert(list, 4); 
        list = insert(list, 5); 
        list = insert(list, 6); 
        list = insert(list, 7); 
        list = insert(list, 8); 

        // Print the LinkedList 
        printList(list); 
    } 
} 
Enter fullscreen mode Exit fullscreen mode

常见面试问题:

  • 反转链接列表

  • 查找链表中的中间值

  • 删除链接列表中的循环


堆栈

是遵循 LIFO(后进先出)顺序的线性数据结构。那么,这是什么意思呢?想象一下一叠盘子。最后一个放在栈顶的盘子是最先取出的。栈就是这样工作的:最后一个添加的值是最先移除的。

把栈想象成一个可以添加和移除元素的集合。栈中最常见的函数是 push、pop、isEmpty 和 peek。

替代文本

堆栈有许多有用的应用:

  • 回溯到之前的状态

  • 表达式求值和转换

常见面试问题:

  • 使用堆栈检查括号是否平衡

  • 在一个数组中实现两个堆栈

  • 使用堆栈获取下一个更大的元素


队列

队列与栈非常相似,因为它们都是具有动态大小的线性数据结构。然而,队列是 FIFO(先进先出)数据结构。为了形象地理解这种数据结构,想象一下你正在排队等待坐过山车。最先排队的人可以离开队伍去乘坐过山车。

在这个数据结构中,元素从“后面”进入,从“前面”离开。队列中的标准函数是入队、出队、后队、前队和 isFull。

替代文本

队列有许多有用的应用:

  • 当一个资源被多个消费者共享时

  • 创建目录

  • 当数据在两个资源之间异步传输时

常见面试问题:

  • 反转队列的前 k 个元素

  • 使用队列生成从 1 到 n 的二进制数


图表

是通过边连接的顶点(节点)的集合,从而形成网络

替代文本

在上面的例子中,顶点集为(12、2、4、18、23),边为(12-2、12-4、2-4、4-18、4-23、18-23、2-18)。

图是一种用途广泛的数据结构,可以解决许多实际问题。图常用于 LinkedIn 或 Facebook 等社交网络。随着 GraphQL 的兴起,数据正以图或网络的形式进行组织。

图的基本实现:

import java.util.*; 

class Graph { 

    // A utility function to add an edge in an 
    // undirected graph 
    static void addEdge(ArrayList<ArrayList<Integer> > adj, 
                        int u, int v) 
    { 
        adj.get(u).add(v); 
        adj.get(v).add(u); 
    } 

    // A utility function to print the adjacency list 
    // representation of graph 
    static void printGraph(ArrayList<ArrayList<Integer> > adj) 
    { 
        for (int i = 0; i < adj.size(); i++) { 
            System.out.println("\nAdjacency list of vertex" + i); 
            for (int j = 0; j < adj.get(i).size(); j++) { 
                System.out.print(" -> "+adj.get(i).get(j)); 
            } 
            System.out.println(); 
        } 
    } 

    // Driver Code 
    public static void main(String[] args) 
    { 
        // Creating a graph with 5 vertices 
        int V = 5; 
        ArrayList<ArrayList<Integer> > adj  
                    = new ArrayList<ArrayList<Integer> >(V); 

        for (int i = 0; i < V; i++) 
            adj.add(new ArrayList<Integer>()); 

        // Adding edges one by one 
        addEdge(adj, 0, 1); 
        addEdge(adj, 0, 4); 
        addEdge(adj, 1, 2); 
        addEdge(adj, 1, 3); 
        addEdge(adj, 1, 4); 
        addEdge(adj, 2, 3); 
        addEdge(adj, 3, 4); 

        printGraph(adj); 
    } 
} 
Enter fullscreen mode Exit fullscreen mode

常见面试问题:

  • 找到两个顶点之间的最短路径

  • 检查两个顶点之间是否存在路径

  • 在图中寻找“母顶点”


哈希表

什么是哈希?
在深入研究哈希表之前,我们有必要先了解一下什么是哈希。

哈希算法是将对象分配到唯一索引(称为键)的过程。每个对象都使用键值对进行标识,对象的集合称为字典。

哈希表的实现方式是将元素存储在数组中,并通过键来标识它们。哈希函数接收一个键,并返回存储该值的索引。

因此,无论何时将键输入哈希函数,它总是会返回相同的索引,该索引将标识关联的元素。此外,如果哈希函数收到一个返回已使用索引的唯一键,则可以创建一个带有链表的元素链。

替代文本

哈希表有许多有用的应用:

  • 当一个资源被多个消费者共享时

  • 密码验证

  • 链接文件名和路径

常见面试问题:

  • 查找数组中的对称对

  • 使用散列对列表进行并集和交集


二叉搜索树

叉搜索树是由节点组成的二叉树数据结构。节点的排列具有以下属性:

  • 左子节点始终包含小于父节点的值。

  • 右子节点始终包含大于父节点的值。

  • 两个子节点也将是二叉搜索树。

二叉搜索树在许多搜索应用中都有使用,也用于确定 3D 游戏中需要渲染的对象。由于分层数据非常常见,这种数据结构在工程项目中被广泛使用。

替代文本

常见操作:

  • 搜索 - 搜索元素

  • 插入——在树中插入一个元素

  • 前序遍历——以前序方式遍历树

  • 中序遍历——按顺序遍历树

  • 后序遍历——以后序方式遍历树

常见面试问题:

  • 在二叉搜索树中查找第 k 个最大值

  • 在二叉搜索树中查找最小值

  • 使用广度优先搜索遍历给定目录

继续学习。

无需费力翻看视频或钻研数百道题目,即可轻松掌握面试常见问题。Educative 的文本课程易于浏览,并配备实时编程环境,让学习更加快速高效。

深入理解编码面试:编码问题模式

替代文本

要学习的重要算法

递归

递归是一种函数直接或间接调用自身的实践。被调用的相应函数称为递归函数。虽然递归通常与算法相关,但将其理解为解决问题的一种方法或方法论可能更有帮助。

那么递归为什么有用呢?或者说它到底是什么?让我们以计算阶乘为例,看看如何使用递归。

public static long factorial(int number){        
        //base case - factorial of 0 or 1 is 1
        if(number <=1){
            return 1;
        }        
        return number*factorial(number - 1);
    }
Enter fullscreen mode Exit fullscreen mode

在上面的例子中,函数从数字 n 开始。当函数被调用时,它将调用 factorial(n - 1)。假设 n 的值为 4,函数将返回

$4 * 阶乘(3) -> 4 * 3 * 阶乘(2) -> 4 * 3 * 2 * 阶乘(1)$

一旦 $n = 1$,函数将返回 $factorial(1) = 1$,我们得到 $factorial(4)$ 等于 $4 * 3 * 2 * 1$,即 24。

在这里,你可以看到递归的威力。递归是一种广泛使用的方法,它将一个复杂的问题分解成更小的子问题,直到我们能够解决它。使用递归,你可以简化许多原本难以解决的复杂问题。

需要更多解释?阅读我们的“什么是递归?”。 Edpresso 截图在这里。


冒泡排序

冒泡排序是一种简单的排序算法,如果相邻元素的顺序不正确,则交换它们。该算法将多次迭代数组,直到元素的顺序正确为止。

假设我们有一个如下所示的数组。

替代文本

当算法从索引 0 开始从左到右扫描数组进行第一次迭代时,它会比较索引 $i$ 与索引 $i + 1$。在索引 1 处,它会发现 11 大于 6,并将两者交换。

替代文本

随着算法继续进行第一次迭代扫描,它将发现 13 大于 10,并将两者交换。

替代文本

接下来,它将对数组进行第二次迭代。当它发现 11 大于 10 时,它将交换索引 2 和 3 中的值。

替代文本

算法将对数组进行第三次迭代扫描,并且由于第三次迭代不需要再进行任何交换,因此算法将结束。

替代文本

可以看出,冒泡排序在处理大量元素时性能不佳,因此它主要用作教学工具。它的运行时复杂度为 O(n^2)。

实现冒泡排序:

public static void bubble_srt(int array[]) {
        int n = array.length;
        int k;
        for (int m = n; m >= 0; m--) {
            for (int i = 0; i < n - 1; i++) {
                k = i + 1;
                if (array[i] > array[k]) {
                    swapNumbers(i, k, array);
                }
            }
            printNumbers(array);
        }
    }
Enter fullscreen mode Exit fullscreen mode

选择排序

选择排序是一种将元素集合分为已排序和未排序的算法。在每次迭代中,该算法在未排序组中找到最小的元素,并将其移动到已排序组的末尾。

让我们看一个例子:

替代文本

首先,所有元素都是未排序的。在第一次迭代中,算法将遍历每个元素,并将 4 确定为最小值。算法会将未排序组中的第一个元素 11 与未排序组中的最小元素 4 进行交换。

替代文本

现在,排序组是索引 0,未排序组是索引 1 到索引 3。对于第二次迭代,算法将从索引 1 开始扫描数组,并将 6 标识为未排序组中的最小值。它将交换 11 和 6。

替代文本

现在,已排序组位于索引 0 到索引 1 之间,未排序组位于索引 2 到索引 3 之间。第三次迭代时,算法将从索引 2 开始,找到 11 作为最小值。由于 11 已经在正确的索引中,因此不会移动。至此,算法结束。

替代文本

与冒泡排序类似,选择排序通常是一种较慢的算法。它的运行时复杂度也为 O(n^2)。

实现选择排序:

public static int[] doSelectionSort(int[] arr){

        for (int i = 0; i < arr.length - 1; i++)
        {
            int index = i;
            for (int j = i + 1; j < arr.length; j++)
                if (arr[j] < arr[index]) 
                    index = j;

            int smallerNumber = arr[index];  
            arr[index] = arr[i];
            arr[i] = smallerNumber;
        }
        return arr;
    }
}
Enter fullscreen mode Exit fullscreen mode

插入排序

插入排序是一种简单的排序算法,它通过一次对一个元素进行排序来构建最终的数组。它是如何工作的?

  • 检查每个元素并将其与左侧的排序元素进行比较

  • 按照排序元素的正确顺序插入项目

我们来看一个例子

替代文本

算法从索引 0 开始,值为 11。由于 11 左侧没有元素,所以它保持原样。现在,转到索引 1。它左侧的值为 11,这意味着我们交换 11 和 4。

替代文本

再次,算法查找 4 的左侧。由于 4 左侧没有元素,所以它停留在原处。接下来,查找索引 2。值为 6 的元素向左查找。由于它小于 11,所以两者互换。

替代文本

元素 6 再次向左查找,但由于 4 小于 6,它停留在原处。接下来,我们转到索引 4 处的元素。算法再次向左查找,但由于 11 小于 13,它停留在原处。现在,算法完成了。

替代文本

插入排序几乎总是比冒泡排序和选择排序更高效,因此在处理少量元素时更常用。与其他两种排序算法类似,插入排序的运行时间也是 O(n^2) 的平方级数。

实现插入排序:

public static int[] doInsertionSort(int[] input){

        int temp;
        for (int i = 1; i < input.length; i++) {
            for(int j = i ; j > 0 ; j--){
                if(input[j] < input[j-1]){
                    temp = input[j];
                    input[j] = input[j-1];
                    input[j-1] = temp;
                }
            }
        }
        return input;
    }
Enter fullscreen mode Exit fullscreen mode

二分查找

二分查找是查找元素最有效的算法。该算法通过比较数组或列表的中间元素与目标元素来工作。如果值相同,则返回元素的索引。如果不相同,则将列表减半。

如果目标值小于中间值,则新列表为左半部分。如果目标值大于中间值,则新列表为右半部分。

这个过程持续进行,你不断分割列表并搜索其中一半,直到搜索算法找到目标值并返回其位置。该算法的运行时复杂度为 O(log2n)。需要注意的是,二分查找仅在列表已排序的情况下有效。

为了形象化二分查找,假设您有一个包含十个元素的排序数组,并且您正在寻找索引 33。

替代文本

这个数组的中间值是16,算法把它和33进行比较,33大于16,所以算法就把数组分割开,在后半部分查找。

替代文本

新的中间值为 28。由于 33 大于 28,因此算法在数组的后半部分进行搜索。

替代文本

将数组再次拆分为右半部分后,新的中间值为 33。算法发现中间值与目标值相同,因此返回元素的位置。

实现二分查找:

int binarySearch(int arr[], int l, int r, int x) 
    { 
        if (r >= l) { 
            int mid = l + (r - l) / 2; 

            // If the element is present at the 
            // middle itself 
            if (arr[mid] == x) 
                return mid; 

            // If element is smaller than mid, then 
            // it can only be present in left subarray 
            if (arr[mid] > x) 
                return binarySearch(arr, l, mid - 1, x); 

            // Else the element can only be present 
            // in right subarray 
            return binarySearch(arr, mid + 1, r, x); 
        } 

        // We reach here when element is not present 
        // in array 
        return -1; 
    } 
Enter fullscreen mode Exit fullscreen mode

其他主题

请记住,我们今天介绍的概念只是入门介绍。深入理解这些概念并练习编程题非常重要。以下是我们建议您熟悉的其他主题:

如何在下一次编程面试中脱颖而出

  • 提前计划:在做任何事情之前,你首先应该做的就是制定一个全面的学习计划。确定你认为需要更多练习的主题,并制定一个可以坚持的学习计划。你应该做好持续学习的准备。一般来说,四到六周是准备面试的最佳时间。

  • 编程语言:选择一种你觉得用起来顺手的编程语言。你能用这种语言在 25 分钟左右轻松编写出解决方案吗?开发人员会使用的一些流行语言包括 Java、Python、JavaScript 或 C++。

  • 公司:在参加任何面试之前,你都应该了解这家公司。了解公司的文化和价值观至关重要,这样才能确保你与公司保持一致,并在面试的非技术方面做好更充分的准备。你还应该研究公司测试的常见问题。通过这样做,你可以进行有针对性的练习,为面试做好准备。

  • 行为面试:即使你觉得自己已经准备好应对技术面试,也别忘了行为面试。公司希望通过行为面试来确认你是否适合该职位和公司。查看我们的行为面试指南,为面试做好充分准备。

现在,你应该对面试前需要做的事情有了大致的了解。你还有很多东西要学,不过不用担心。如果你想继续学习,可以查看“编码面试”提供的资源,或者查看我们“编程语言面试”系列下的“Python”“Java”“JavaScript” 面试准备课程

资源

这是有关编码面试和一般计算机科学主题的资源列表。

文章

文章来源:https://dev.to/educative/top-data-structs-and-algorithms-every-developer-must-know-241a
PREV
哪些软件技术能为您带来最高薪资?我应该先学哪种编程语言?接下来应该学哪种编程语言?前端开发人员:学完 JavaScript 之后我应该学什么?探索这些框架和库 提升您的技能 找到适合您的数据库管理系统 云端开发 移动操作系统:Android 和 iOS 结论 下一步是什么?
NEXT
初级开发人员生存指南:成功的三大支柱 第 1 部分:成功的三大支柱 第 2 部分:工作中需要注意的十大危险信号 第 3 部分:十个小贴士 第 4 部分:初级开发人员资源指南