问答题:
- 什么是数据结构?请举例说明至少两种基本类型的数据结构。
- 解释数组与链表之间的区别。
- 如何用栈实现一个队列?
- 解释哈希表的工作原理,并给出其一个使用场景。
- 什么是二叉树?请说明二叉搜索树的特点。
- 能否编写一个快速排序的伪代码?
- 请说明图的两种表示方法,并比较它们的优劣。
- 动态数组和静态数组有什么区别?
- 如何检测一个链表中的环?
- 解释堆和二叉堆的概念,以及它们在优先队列中的应用。
编程题:
- 实现链表反转
编写一个函数来反转一个单链表。 - 检测循环依赖
在给定的有向图中,编写一个函数来检测是否存在循环依赖。 - 合并有序数组
有两个排序的整数数组A和B,A的末端有足够的空间容纳B。编写一个函数将B合并入A并排序。 - 最小栈
设计一个栈,除了常规栈支持的操作外,还需要支持返回栈内最小元素的操作。 - 平衡括号
给出一个字符串,其中包含'(',')','{','}','['和']'这些字符,判断字符串中的括号是否配对且平衡。 - 岛屿数量
给你一个由'1'(陆地)和'0'(水)组成的的二维网格,请计算网格中岛屿的数量。 - 前K个高频元素
给定一个非空的整数数组,返回其中出现频率前k高的元素。 - LRU缓存实现
使用哈希表加双向链表可以实现这个要求。哈希表用于快速定位元素,双向链表用于表示元素之间的先后顺序。
问答题答案:
- 数据结构是存储和组织数据的方式,以便可以有效地访问和修改。两种基本数据结构包括数组和链表。
- 数组是将元素顺序存储在内存中的数据结构。
- 链表由一系列节点组成,每个节点包含数据和指向列表中下一个节点的引用。
- 数组与链表的主要区别在于内存分配和元素访问:
- 数组在内存中占用连续空间,可以实现快速的随机访问。
- 链表的元素在内存中不是连续存储的,元素插入和删除操作通常比数组更高效。
- 可以使用两个栈来实现队列,一个栈用来处理入列操作,另一个栈用来处理出列操作。当进行出列操作时,如果出列用的栈为空,就将入列栈中所有元素弹出并压入出列栈中。
- 哈希表是一种通过哈希函数将键映射到表中一个位置来访问记录的数据结构,这样可以通过键直接访问到存储的值。它的一个典型使用场景是提高快速查找和插入数据项的效率。
- 二叉树是每个节点最多有两个子节点的树结构。二叉搜索树(BST)是一种特殊的二叉树,其中左子树上的所有元素都小于其根节点的值,而右子树上的所有元素都大于其根节点的值。
- 快速排序是一种分而治之的排序算法,通过一个分区操作将数据分为独立的两部分,其中一部分的所有数据都比另一部分的小,然后递归地在两部分进行快速排序。
- 图通常有两种表示方法:邻接矩阵和邻接表。
- 邻接矩阵是一个二维数组,表示图中节点之间的连接情况。
- 邻接表是一个链表数组,表示每个节点的邻接点。
- 动态数组可以在运行时动态增加容量,而静态数组在声明时大小已确定,不能改变。
- 检测链表中的环通常使用弗洛伊德的乌龟和兔子(快慢指针)算法,即同时使用两个速度不同的指针遍历链表,如果存在环,那么这两个指针一定会相遇。
- 堆是一种特殊的完全二叉树,其中每个父节点的值都小于或等于(在最小堆中)或大于或等于(在最大堆中)它的子节点的值。二叉堆的常见应用是作为优先队列的实现结构,允许快速插入新元素和快速移除最小或最大元素。
编程题答案:
- 实现链表反转
可以通过迭代方法,初始化三个变量prev
、current
和next
分别用以存储前一个节点、当前节点和下一个节点。在遍历链表时不断更新这些变量以实现链表的反转。typedef struct ListNode { int val; struct ListNode *next; } ListNode; ListNode* reverseList(ListNode* head) { ListNode *prev = NULL; ListNode *current = head; ListNode *next = NULL; while (current != NULL) { next = current->next; current->next = prev; prev = current; current = next; } return prev; }
- 检测循环依赖
利用深度优先搜索(DFS)来检测图中是否存在环。通过维护一个访问状态数组,对每个节点进行dfs访问,如果在DFS遍历过程中访问到了状态为“正在访问”的节点,则存在环。int canFinish(int numCourses, int** prerequisites, int prerequisitesSize, int* prerequisitesColSize) { // ... 代码中需要包含图的构建以及访问状态数组的初始化 ... // visit数组的元素初始值通常设为0 int* visit = (int*)malloc(sizeof(int) * numCourses); // 构建图和初始化visit等操作 ... // DFS函数的实现 int isCyclic(int u, int* visit, int** graph, int* graphColSize) { if(visit[u] == 1) return 0; // 不是环 if(visit[u] == -1) return 1; // 是环 visit[u] = -1; // 标记为正在访问 for(int j = 0; j < graphColSize[u]; ++j) { if(isCyclic(graph[u][j], visit, graph, graphColSize)) // DFS deeper return 1; } visit[u] = 1; // 标记为已访问 return 0; } // 对每个节点进行环检测 for(int i = 0; i < numCourses; ++i) { if(isCyclic(i, visit, /*图和它的大小*/)) { return 0; // 有环,不能完成 } } return 1; // 无环,可以完成 }
- 合并有序数组
从两个数组的最后一位开始比较,并将比较后较大的数放到数组A的最后面,直到数组B的所有元素都被合并进数组A。void merge(int* nums1, int m, int* nums2, int n) { int i = m - 1, j = n - 1, tar = m + n - 1; while (j >= 0) { if (i >= 0 && nums1[i] > nums2[j]) { nums1[tar--] = nums1[i--]; } else { nums1[tar--] = nums2[j--]; } } }
- 最小栈
维护一个辅助栈,这个栈用来存储每个元素入栈时当前栈的最小值。在每次元素入栈操作时将当前最小值一起放入辅助栈。typedef struct { int *elements; int *minElements; int top; int minTop; int capacity; // 栈的容量,根据需要初始化 } MinStack; MinStack* minStackCreate() { MinStack *stack = (MinStack *)malloc(sizeof(MinStack)); stack->elements = (int *)malloc(sizeof(int) * someSize); // 初始化为一定大小 stack->minElements = (int *)malloc(sizeof(int) * someSize); stack->top = -1; stack->minTop = -1; stack->capacity = someSize; return stack; } void minStackPush(MinStack* obj, int val) { if (obj->top == obj->capacity - 1) return; // 栈满 obj->elements[++(obj->top)] = val; if (obj->minTop == -1 || val <= obj->minElements[obj->minTop]) { obj->minElements[++(obj->minTop)] = val; } } void minStackPop(MinStack* obj) { if (obj->top == -1) return; // 栈空 int val = obj->elements[(obj->top)--]; if (val == obj->minElements[obj->minTop]) { obj->minTop--; } } int minStackTop(MinStack* obj) { if (obj->top == -1) return -1; // 栈空 return obj->elements[obj->top]; } int minStackGetMin(MinStack* obj) { if (obj->minTop == -1) return -1; // 栈空 return obj->minElements[obj->minTop]; } void minStackFree(MinStack* obj) { free(obj->elements); free(obj->minElements); free(obj); }
- 平衡括号
使用栈遍历字符串,如果遇到开括号就入栈,如果遇到闭括号并且栈顶是对应的开括号,则两者抵消,否则不平衡。#include <stdbool.h> #include <stdlib.h> bool isValid(char * s) { int len = 0; while (s[len] != '\0') len++; char *stack = (char *)malloc(len); int top = -1; for (int i = 0; s[i] != '\0'; ++i){ switch (s[i]) { case '(': case '{': case '[': stack[++top] = s[i]; break; case ')': if (top == -1 || stack[top] != '(') return false; --top; break; case '}': if (top == -1 || stack[top] != '{') return false; --top; break; case ']': if (top == -1 || stack[top] != '[') return false; --top; break; } } free(stack); return top == -1; }
- 岛屿数量
对于每个为'1'的单元,通过DFS将与之相连的所有'1'都标记为已访问,以此来识别一个岛屿。对整个网格进行遍历,计算这样的岛屿数量。void dfs(char** grid, int gridSize, int* gridColSize, int r, int c) { int nr = gridSize; int nc = *gridColSize; if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') { return; } grid[r][c] = '0'; dfs(grid, gridSize, gridColSize, r - 1, c); // up dfs(grid, gridSize, gridColSize, r + 1, c); // down dfs(grid, gridSize, gridColSize, r, c - 1); // left dfs(grid, gridSize, gridColSize, r, c + 1); // right } int numIslands(char** grid, int gridSize, int* gridColSize) { if (grid == NULL || gridSize == 0) { return 0; } int num_islands = 0; for (int r = 0; r < gridSize; r++) { for (int c = 0; c < gridColSize[r]; c++) { if (grid[r][c] == '1') { ++num_islands; dfs(grid, gridSize, gridColSize, r, c); } } } return num_islands; }
- 前K个高频元素 在C语言中实现这样一个功能需要用到一些额外的数据结构来帮助我们进行存储和排序。由于C语言中没有内建这样的数据结构,我们需要使用数组来辅助实现。 以下是一个使用快速排序变种(基于频率的排序)的示例代码。 首先,我们需要一个结构来存储整数及其频率:
typedef struct { int num; int freq; } NumFreq;
在进行排序之前,我们需要统计每个整数出现的频率:
void countFrequency(int* nums, int numsSize, NumFreq* freqArr) { for (int i = 0; i < numsSize; ++i) { freqArr[nums[i]].num = nums[i]; freqArr[nums[i]].freq++; } }
对
NumFreq
数组按频率进行降序排序,我们可以使用快速排序算法:int partition(NumFreq* arr, int low, int high) { int pivot = arr[high].freq; int i = (low - 1); for (int j = low; j <= high - 1; j++) { if (arr[j].freq > pivot) { i++; NumFreq temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } NumFreq temp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = temp; return (i + 1); } void quickSort(NumFreq* arr, int low, int high) { if (low < high) { int pi = partition(arr, low, high); quickSort(arr, low, pi - 1); quickSort(arr, pi + 1, high); } }
最后,我们可以提取频率最高的K个元素:
int* topKFrequent(int* nums, int numsSize, int k, NumFreq* freqArr, int* returnSize) { countFrequency(nums, numsSize, freqArr); quickSort(freqArr, 0, numsSize - 1); int* result = malloc(k * sizeof(int)); *returnSize = k; for (int i = 0; i < k; ++i) { result[i] = freqArr[i].num; } return result; }
- LRU缓存实现
- 定义一个链表节点结构体,它应该包含键,值,以及指向前一个节点和后一个节点的指针。
- 定义LRU缓存的结构体,它应该有一个上述链表节点类型的指针,指向链表的头部和尾部,还有一个整数来记录缓存的当前大小和缓存的容量限制。
- 为双向链表实现基础功能函数,如添加节点到头部,移除末尾节点,以及移动节点到头部。
- 实现用于LRU缓存的功能函数,如插入键值对,获取键对应的值。下面是一个简化的C语言伪代码,用于演示LRU缓存的基本逻辑:
#include <stdio.h> #include <stdlib.h> typedef struct Node { int key; int value; struct Node* prev; struct Node* next; } Node; typedef struct { Node* head; Node* tail; int capacity; int size; Node** hash; // 假设hash是一个数组,为简化起见,不展示哈希表实现 } LRUCache; LRUCache* lRUCacheCreate(int capacity) { LRUCache* obj = malloc(sizeof(LRUCache)); obj->head = NULL; obj->tail = NULL; obj->capacity = capacity; obj->size = 0; // 需要为hash分配空间并初始化 return obj; } int lRUCacheGet(LRUCache* obj, int key) { Node* node = obj->hash[key]; if (node) { // 将节点移到头部 return node->value; } else { return -1; // 表示不在缓存中 } } void lRUCachePut(LRUCache* obj, int key, int value) { Node* node = obj->hash[key]; if (node) { // 更新节点的值 // 将节点移到头部 } else { if (obj->size == obj->capacity) { // 移除尾部节点 // 从哈希表中移除对应的项 } // 创建新节点 // 插入到头部 // 添加到哈希表 } } void lRUCacheFree(LRUCache* obj) { // 释放所有节点和LRUCache对象 }