淘米科技一面面经
来源:https://www.nowcoder.com/feed/main/detail/3bddc6d3ebb745be8d8690569d934dad
1、线性结构和非线性结构区别?
- 线性结构:
- 线性结构中的数据元素之间存在顺序关系,每个元素都有一个前驱和一个后继,除了第一个元素和最后一个元素。
- 线性结构中的常见数据结构包括数组、链表、栈和队列。
- 示例:数组中的元素按顺序排列,链表中的节点有一个后继指针,栈和队列中的元素按照一定的顺序进出。
- 非线性结构:
- 非线性结构中的数据元素之间没有严格的顺序关系,元素之间的联系复杂,可能存在多个前驱或后继。
- 非线性结构中的常见数据结构包括树和图。
- 示例:树中的节点可以有多个子节点,图中的节点之间可以存在各种类型的关系。
2、数组和链表区别?
- 存储方式:
- 数组:数组是一种连续的存储结构,元素在内存中按照线性顺序排列。这使得数组支持随机访问,可以通过索引快速访问任何元素。
- 链表:链表是一种非连续的存储结构,元素以节点的形式存在,每个节点包含数据和指向下一个节点的指针。链表不支持随机访问,必须从头节点开始遍历才能找到特定元素。
- 大小固定性:
- 数组:数组的大小通常是固定的,一旦分配了内存空间,就不容易动态扩展或缩小。在某些编程语言中,可以使用动态数组来解决这个问题,如C++中的
std::vector。 - 链表:链表的大小可以动态增长或减小,节点可以根据需要进行动态分配和释放。
- 数组:数组的大小通常是固定的,一旦分配了内存空间,就不容易动态扩展或缩小。在某些编程语言中,可以使用动态数组来解决这个问题,如C++中的
- 插入和删除操作:
- 数组:在数组中插入或删除元素通常需要移动其他元素,这可能涉及到大量的数据搬迁,导致性能下降。
- 链表:在链表中插入或删除元素只需要调整节点的指针,不需要移动大量的数据,因此插入和删除操作通常更高效。
- 空间效率:
- 数组:由于数组是连续存储,可能会浪费一些内存空间,特别是当数组的大小大于实际需要的元素数量时。
- 链表:链表通常比数组更加灵活,不会浪费太多内存空间,因为它可以根据需要动态分配节点。
- 随机访问:
- 数组:支持O(1)时间复杂度的随机访问,即通过索引可以直接访问元素。
- 链表:不支持O(1)时间复杂度的随机访问,必须从头节点开始遍历才能找到特定元素,平均时间复杂度为O(n)。
3、单链表,如何找到中间的节点?
要找到单链表的中间节点,你可以使用双指针技巧,其中一个指针每次移动一个节点,另一个指针每次移动两个节点。当快指针到达链表尾部时,慢指针就会指向链表的中间节点。
参考代码:
#include <iostream>
struct ListNode {
int data;
ListNode* next;
ListNode(int val) : data(val), next(nullptr) {}
};
ListNode* findMiddle(ListNode* head) {
if (head == nullptr) {
return nullptr;
}
ListNode* slow = head;
ListNode* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next; // 慢指针每次移动一个节点
fast = fast->next->next; // 快指针每次移动两个节点
}
return slow;
}
int main() {
// 创建一个单链表:1 -> 2 -> 3 -> 4 -> 5
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
head->next->next->next = new ListNode(4);
head->next->next->next->next = new ListNode(5);
// 找到中间节点
ListNode* middle = findMiddle(head);
if (middle != nullptr) {
std::cout << "中间节点的值为: " << middle->data << std::endl;
} else {
std::cout << "链表为空或长度为偶数,没有中间节点。" << std::endl;
}
// 释放链表内存
while (head != nullptr) {
ListNode* temp = head;
head = head->next;
delete temp;
}
return 0;
}4、时间复杂度的概念,如何计算?
时间复杂度通常用大O符号(O)来表示,表示算法的运行时间与输入规模之间的增长关系。常见的时间复杂度包括 O(1)、O(log n)、O(n)、O(n log n)、O(n^2)、O(2^n) 等。
- O(1) - 常数时间复杂度:算法的运行时间是常数,与输入规模无关。例如,访问数组中的元素。
- O(log n) - 对数时间复杂度:算法的运行时间随输入规模呈对数增长。例如,二分查找。
- O(n) - 线性时间复杂度:算法的运行时间与输入规模成正比。例如,遍历数组。
- O(n log n) - 线性对数时间复杂度:算法的运行时间随输入规模呈 n log n 增长。例如,快速排序和归并排序。
- O(n^2) - 平方时间复杂度:算法的运行时间与输入规模的平方成正比。例如,简单的嵌套循环。
- O(2^n) - 指数时间复杂度:算法的运行时间随输入规模成指数级增长。例如,暴力解法。
计算时间复杂度通常涉及分析算法中的循环和递归结构。你需要考虑算法中执行次数最多的那部分代码,并用输入规模 n 来表示。在分析过程中,通常忽略常数因子和低阶项,只关注增长最快的项,因为它们在大规模数据下占主导地位。
5、知道哪些排序算法?快排的时间复杂度多少?
冒泡排序(Bubble Sort):
冒泡排序是一种简单的比较排序算法,它多次遍历待排序数组,依次比较并交换相邻元素,使最大(或最小)的元素逐渐“浮”到数组的末尾。
时间复杂度:平均情况和最坏情况均为O(n^2)。
选择排序(Selection Sort):
选择排序是一种简单的排序算法,它从待排序数组中选择最小的元素,然后将它与数组的第一个元素交换,接着从剩下的元素中选择次小的元素,重复这个过程。
时间复杂度:平均情况和最坏情况均为O(n^2)。
插入排序(Insertion Sort):
插入排序是一种简单的排序算法,它从未排序部分取出一个元素,将其插入已排序部分的适当位置,重复这个过程。
时间复杂度:平均情况和最坏情况均为O(n^2)。
归并排序(Merge Sort):
归并排序是一种分治排序算法,它将数组分成两部分,分别排序,然后将已排序的部分合并。
时间复杂度:平均情况和最坏情况均为O(n log n)。
堆排序(Heap Sort):
堆排序使用堆数据结构进行排序。它首先将数组构建成最大堆或最小堆,然后将堆顶元素与堆底元素交换,重复这个过程。
时间复杂度:O(n log n)。
计数排序(Counting Sort):
计数排序适用于已知输入范围的整数排序。它创建一个计数数组来存储每个元素的出现次数,然后根据计数数组构建排序后的数组。
时间复杂度:O(n + k),其中 k 是输入范围。
桶排序(Bucket Sort):
桶排序将输入数据分布到多个桶中,然后对每个桶中的数据进行排序,最后合并桶中的数据。
时间复杂度:平均情况为O(n + n^2/k + k),其中 k 是桶的数量。
基数排序(Radix Sort):
基数排序是一种非比较排序算法,它按照数字位数的顺序,从最低位到最高位进行排序。可以用于整数或字符串排序。
时间复杂度:O(d * (n + k)),其中 d 是数字的位数,k 是每个位数的基数。
快速排序(Quick Sort):
- 快速排序是一种分治排序算法,它选择一个基准元素,将小于基准的元素放在左边,大于基准的元素放在右边,然后递归地对左右两侧的子数组进行排序。
时间复杂度:
- 平均情况时间复杂度: O(n log n)
- 最坏情况时间复杂度: O(n^2) - 当待排序的数据已经有序或近乎有序时。
- 最好情况时间复杂度: O(n log n) - 当待排序的数据被均匀地分割成两半。
工作原理:
- 选择基准元素(通常是待排序数组的第一个元素)。
- 将小于基准的元素移到基准的左边,将大于基准的元素移到基准的右边。这一步称为划分(Partitioning)。
- 对基准左右两侧的子数组分别递归地应用快速排序。
- 重复上述步骤,直到每个子数组只包含一个元素或为空。
工作原理示例: 假设我们有一个待排序数组 [7, 2, 1, 6, 8, 5, 3, 4]:
- 我们选择基准元素,例如选择第一个元素 7。
- 进行划分,将小于 7 的元素移到左边,大于 7 的元素移到右边。现在数组可能变成
[2, 1, 6, 5, 3, 4, 7, 8]。 - 左右两侧的子数组分别为
[2, 1, 6, 5, 3, 4]和[8]。 - 我们递归地对左右两侧的子数组应用快速排序,重复上述步骤。
- 最终,整个数组被排序。
时间复杂度解释:
- 平均情况下,每次划分可以将数组分为大致相等的两部分,因此总的递归深度为 O(log n)。每次划分需要 O(n) 的时间,因此平均时间复杂度为 O(n log n)。
- 最坏情况下,如果每次划分都不平衡,导致递归树退化为一棵高度为 n 的树,时间复杂度为 O(n^2)。
- 最好情况下,每次划分都能均匀地分割数组,递归深度也是 O(log n),所以最好情况下的时间复杂度也是 O(n log n)。
6、选一个熟悉的排序算法进行实现?
这里给大家写一下归并排序吧,像什么冒泡、快排都写的轻车熟路了。
思路:
- 将待排序数组分为两半,直到每个子数组只包含一个元素。
- 递归地对左半部分和右半部分进行排序。
- 合并两个有序子数组为一个大的有序数组。
参考代码:
#include <iostream>
#include <vector>
// 合并两个有序数组为一个有序数组
void merge(std::vector<int>& arr, int left, int middle, int right) {
int n1 = middle - left + 1;
int n2 = right - middle;
std::vector<int> leftArr(n1);
std::vector<int> rightArr(n2);
// 将数据拷贝到左右两个子数组中
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int i = 0; i < n2; i++) {
rightArr[i] = arr[middle + 1 + i];
}
// 归并过程
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
// 处理剩余元素
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
}
// 归并排序主函数
void mergeSort(std::vector<int>& arr, int left, int right) {
if (left < right) {
int middle = left + (right - left) / 2;
// 递归排序左右两半
mergeSort(arr, left, middle);
mergeSort(arr, middle + 1, right);
// 合并已排序的两部分
merge(arr, left, middle, right);
}
}
int main() {
std::vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
int n = arr.size();
std::cout << "Original Array: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
mergeSort(arr, 0, n - 1);
std::cout << "Sorted Array: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}7、找出最大连续了数组,返回最大和,以及最大和对应的子数组?
思路:
- 遍历数组,同时维护两个变量,
maxSoFar表示到目前为止的最大子数组和,maxEndingHere表示包括当前元素的最大子数组和。 - 对于每个元素,计算
maxEndingHere = max(nums[i], maxEndingHere + nums[i]),即要么从当前元素重新开始,要么将当前元素加入到之前的子数组中。 - 在每一步中,更新
maxSoFar,使其等于max(maxSoFar, maxEndingHere)。 - 最终
maxSoFar将包含整个数组的最大子数组和。
示例代码:
#include <iostream>
#include <vector>
// 函数返回最大子数组和以及最大子数组的起始和结束索引
std::pair<int, std::pair<int, int>> maxSubarraySum(const std::vector<int>& nums) {
int maxSoFar = nums[0];
int maxEndingHere = nums[0];
int start = 0;
int end = 0;
int tempStart = 0;
for (int i = 1; i < nums.size(); i++) {
if (nums[i] > maxEndingHere + nums[i]) {
maxEndingHere = nums[i];
tempStart = i;
} else {
maxEndingHere += nums[i];
}
if (maxEndingHere > maxSoFar) {
maxSoFar = maxEndingHere;
start = tempStart;
end = i;
}
}
return std::make_pair(maxSoFar, std::make_pair(start, end));
}
int main() {
std::vector<int> nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
auto result = maxSubarraySum(nums);
int maxSum = result.first;
int startIndex = result.second.first;
int endIndex = result.second.second;
std::cout << "Maximum Subarray Sum: " << maxSum << std::endl;
std::cout << "Maximum Subarray: ";
for (int i = startIndex; i <= endIndex; i++) {
std::cout << nums[i] << " ";
}
std::cout << std::endl;
return 0;
}8、口述思路:单链表成环,如何判断环的长度?
判断单链表中环的长度,可以使用快慢指针法。下面是口述的思路:
- 首先,使用两个指针,一个慢指针(slow)和一个快指针(fast),从链表的头部出发。
- 快指针每次前进两步,而慢指针每次前进一步,直到它们相遇。这里快指针的速度是慢指针的两倍。
- 一旦快慢指针相遇,表明链表中存在环。此时,可以继续移动慢指针,并且使用一个计数器来记录移动的步数,直到慢指针再次回到相遇点。
- 此时,计数器的值就是环的长度。
这个方法的思想是,快指针比慢指针快,所以当它们第一次相遇时,慢指针没有完成一圈,而快指针可能已经走了两圈或更多。所以,当慢指针再次回到相遇点时,计数器的值就等于环的长度。
这个方法的时间复杂度是O(n),其中n是环的长度。
9、排好序的数组去除重复元素? 思路:
- 初始化两个指针,一个指向当前元素,另一个指向下一个元素。
- 检查当前元素和下一个元素是否相等。
- 如果相等,将下一个元素删除,继续检查下一个元素,直到找到不同的元素。
- 如果不同,将第一个指针移到下一个位置,继续检查下一个元素。
参考代码:
#include <vector>
std::vector<int> removeDuplicates(std::vector<int>& sortedArray) {
int n = sortedArray.size();
if (n <= 1) {
return sortedArray; // 如果数组为空或只有一个元素,无需去重
}
int index = 0; // 用于记录非重复元素的位置
for (int i = 1; i < n; i++) {
if (sortedArray[i] != sortedArray[index]) {
index++;
sortedArray[index] = sortedArray[i];
}
}
sortedArray.resize(index + 1); // 调整数组大小,去除重复元素后的大小
return sortedArray;
}
int main() {
std::vector<int> sortedArray = {1, 2, 2, 3, 4, 4, 4, 5, 5, 6, 6};
std::cout << "原始数组:";
for (int num : sortedArray) {
std::cout << num << " ";
}
std::cout << std::endl;
sortedArray = removeDuplicates(sortedArray);
std::cout << "去除重复后的数组:";
for (int num : sortedArray) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}10、get和post区别?
- 数据传输方式:
- GET:通过URL将数据附加在请求中,数据暴露在URL中,通常用于请求数据,对请求参数有长度限制,通常在2048字符以内。
- POST:通过请求正文传输数据,数据不暴露在URL中,通常用于发送数据,对请求参数没有固定的长度限制,可以传输大量数据。
- 安全性:
- GET:传输的数据在URL中可见,因此不适合传输敏感信息,如密码。
- POST:传输的数据在请求正文中,相对更安全,适合传输敏感信息。
- 缓存:
- GET:请求可以被缓存,对相同URL的多次GET请求可以从缓存中获取响应,具有幂等性(多次请求产生相同结果)。
- POST:请求不能被缓存,每次POST请求都会向服务器发送数据,不具有幂等性。
- 幂等性:
- GET:GET请求通常应该是幂等的,即多次执行相同的GET请求应该产生相同的结果。
- POST:POST请求通常不是幂等的,多次执行相同的POST请求可能会导致不同的结果。
- 书签和浏览器历史:
- GET:可以被添加为书签,可以在浏览器历史中记录。
- POST:通常不会被添加为书签,不会出现在浏览器历史中。
- 数据类型:
- GET:通常用于请求资源,如网页、图片等。
- POST:通常用于提交表单数据、上传文件等。
11、403状态码、502状态码、200状态码?
- 403 Forbidden:表示服务器理解客户端的请求,但服务器拒绝执行该请求。常见的原因包括缺少访问权限、身份验证失败等。服务器通常会提供一个页面或错误消息来解释拒绝请求的原因。
- 502 Bad Gateway:表示服务器作为网关或代理,从上游服务器接收到无效的响应。这可能是因为上游服务器宕机或不可用,导致代理服务器无法正常获取响应。
- 200 OK:表示客户端请求已成功。这是最常见的成功状态码,表示服务器已成功处理了请求,并返回相应的响应。
12、tcp和udp区别?
- 连接导向 vs. 无连接:
- TCP是一种面向连接的协议,它在数据传输前需要建立连接,确保数据的可靠性和有序性,然后在传输完成后释放连接。
- UDP是一种无连接的协议,它不需要在传输数据前建立连接,也不维护连接状态,数据包之间相互独立,不会保证数据的可靠性和有序性。
- 可靠性:
- TCP提供可靠的数据传输。它使用序号、确认和重传机制来确保数据包的正确性,确保数据不丢失和按顺序传输。
- UDP不提供可靠性。它只是简单地将数据包从源发送到目标,不提供确认、重传或错误检查。
- 数据量:
- TCP适用于大量数据的可靠传输,如文件传输、网页访问等。
- UDP适用于需要快速传输和可以容忍一些数据丢失的应用,如实时音视频流、在线游戏等。
- 开销:
- TCP协议在建立和维护连接时有较高的开销,因为它需要进行三次握手、四次挥手等操作,以确保可靠性。
- UDP协议的开销较低,因为它不涉及连接的建立和维护。
- 适用场景:
- TCP适用于需要可靠数据传输和流控制的场景,如HTTP、FTP等传统应用。
- UDP适用于对实时性要求较高、可以容忍一些数据丢失的应用,如音频/视频流、在线游戏、DNS查询等。
13、linux命令查看pid,查看某个进程网络连接情况?
- 查看网络连接的工具
netstat:
使用 netstat 命令可以查看所有网络连接情况,包括所有进程的网络连接。可以结合 grep 命令来筛选特定进程的网络连接。
netstat -tuln | grep <进程PID>- 使用
ss命令:
ss 命令是 netstat 的替代工具,它在一些系统上更常用。可以使用如下命令查看特定进程的网络连接:
ss -tuln | grep <进程PID>- 使用
lsof命令:
lsof 命令可以列出打开文件和网络连接的进程信息。你可以使用以下命令查看某个进程的网络连接:
lsof -i -n -P | grep <进程PID>14、操作系统底层,代码编译到运行是如何执行的?
- 预处理(Preprocessing): 在编译过程之前,C/C++编译器会对源代码进行预处理。预处理器处理代码中的预处理指令,例如
#include、#define和条件编译指令(如#ifdef和#ifndef)。预处理器会展开宏,包含其他源文件,删除注释等。处理后的代码称为扩展代码。 - 编译(Compiling): 扩展代码由编译器处理。编译器将源代码翻译成汇编代码或机器代码,这个过程称为编译。编译器会检查代码的语法和语义,生成中间文件或汇编代码。
- 汇编(Assembling): 汇编器接收编译器生成的汇编代码或中间文件,将其翻译成目标二进制代码。结果是一个包含机器指令的目标文件,通常是二进制文件。
- 链接(Linking): 如果你的程序使用了多个源文件,这些源文件会被编译成多个目标文件。链接器将这些目标文件组合成一个可执行文件。它解决了函数调用和全局变量之间的引用,使得它们在程序中能够正确连接。
- 生成可执行文件: 在链接阶段,链接器将目标文件组合成可执行文件。这个可执行文件包含程序的机器代码,入口点(通常是
main函数)以及其他必要的信息,如库和资源。
15、操作系统中,一个程序的内存区域划分?
- 代码段(Text Segment): 代码段存储了程序的机器指令,包括可执行代码。这部分内存通常是只读的,因为程序不应该修改其自身的机器指令。代码段是程序的逻辑执行部分。
- 数据段(Data Segment): 数据段包含程序中的全局变量和静态变量,以及程序的堆(Heap)和栈(Stack)。数据段通常分为以下几个部分:
- 全局变量区(Global Variables):存储全局和静态变量。
- 堆区(Heap):用于动态分配内存。程序员可以手动控制堆上的内存分配和释放。
- 栈区(Stack):用于存储局部变量和函数调用的执行上下文。栈是一个后进先出(LIFO)数据结构,用于管理函数的调用和返回。
- 堆栈段(Heap and Stack): 堆栈段指的是程序的堆和栈。堆用于动态分配内存,允许程序在运行时分配和释放内存。栈用于函数调用和执行上下文管理。这两个部分的管理在程序运行时由编程语言和运行时系统控制。
- 常量段(Constant Segment): 常量段存储程序中的常量数据,如字符串文字。这些数据通常是只读的。
- 代码库段(Library Segment): 如果程序使用了外部库,这部分存储了外部库的代码和数据。
16、b+树和b树区别?
- 数据存储方式:
- B树:B树的每个节点既存储数据也存储索引。这意味着B树的非叶子节点既包含索引键也包含对应的数据。
- B+树:B+树的非叶子节点仅存储索引键,而数据全部存储在叶子节点。B+树的所有叶子节点通过指针连接成一个有序链表,便于范围查询。
- 叶子节点关联性:
- B树:B树的叶子节点和非叶子节点没有直接关联,各自独立存在。
- B+树:B+树的叶子节点之间通过链表相连,形成了一个有序的链表。这有助于范围查询,因为数据按顺序排列。
- 范围查询效率:
- B树:在B树上进行范围查询相对较慢,因为需要访问非叶子节点和叶子节点。
- B+树:B+树在范围查询时非常高效,因为只需遍历叶子节点的链表。
- 非叶子节点的索引键:
- B树:非叶子节点的索引键可以是重复的,可能导致不必要的冗余数据。
- B+树:B+树的非叶子节点的索引键一般是不重复的,确保了索引的唯一性。
- 叶子节点个数:
- B树:B树的叶子节点数量和数据记录数量相等。
- B+树:B+树的叶子节点数量比数据记录数量多,因为叶子节点主要用于存储数据和维护链表,而非叶子节点用于索引。
17、数据库慢查询如何定位,如何优化?
定位慢查询:
- 数据库监控工具: 使用数据库监控工具,如MySQL的Slow Query Log或PostgreSQL的pg_stat_statements,来识别慢查询。这些工具可以记录执行时间较长的查询。
- 查询执行计划: 对于关系型数据库,查看查询执行计划是一种重要的方式来定位性能问题。通过
EXPLAIN或EXPLAIN ANALYZE命令,可以查看数据库是如何执行查询的,以及是否有性能问题。 - 应用层监控: 使用应用层性能监控工具来识别慢查询的来源。这些工具可以告诉你哪些查询正在导致性能下降。
- 监控索引: 确保表上使用了适当的索引。缺少索引或者使用不当的索引可能导致查询变慢。
- 检查服务器负载: 如果服务器负载过高,可能导致查询变慢。检查服务器的CPU、内存和磁盘使用情况。
优化慢查询:
- 索引优化: 确保数据库表上的列有适当的索引,这有助于加速检索。但不要过度索引,因为索引也会增加写操作的开销。
- 重写查询: 有时,可以通过重写查询语句来优化查询性能。使用合适的
WHERE子句、JOIN条件和ORDER BY子句等。 - 缓存: 使用缓存来减少数据库查询的频率。可以使用缓存中间件(如Redis或Memcached)或应用程序级别的缓存。
- 升级硬件: 如果服务器性能不足,可以考虑升级硬件,包括CPU、内存和存储。
- 分区: 对于大型表,可以考虑分区,以减小查询的范围。
- 定期维护: 定期执行数据库维护操作,如索引重建、表优化和统计信息更新,以确保数据库性能不下降。
- 限制结果集大小: 通过限制查询返回的结果集大小,可以减轻数据库负担。
- 异步处理: 将某些查询从同步操作改为异步操作,以提高应用程序的响应速度。
- 数据库分片: 对于大型数据库,可以考虑数据库分片,将数据分散到多个服务器上以提高查询性能。
- 使用合适的数据库引擎: 不同的数据库引擎具有不同的性能特点。根据应用程序需求选择合适的数据库引擎。
18、索引失效场景?
- 使用函数或运算符处理列: 当在查询条件中对列进行函数调用或进行运算时,索引通常会失效。例如,
WHERE DATE_FORMAT(column, '%Y-%m-%d') = '2022-10-28',这种情况下,无法使用列上的索引。 - 使用通配符前缀: 当在查询条件中使用通配符前缀(如
LIKE 'prefix%')时,通常无法使用索引。 - 使用
OR条件: 在WHERE子句中使用多个OR条件时,如果每个条件都没有适用的索引,那么索引可能会失效。 - 不等于条件: 当使用不等于条件(
!=或<>)时,索引通常会失效。 - 数据分布不均匀: 如果索引列上的数据分布极不均匀,即某些值出现非常频繁,而其他值出现很少,索引可能会失效。这会导致查询优化器认为扫描整个表更快。
- 大数据表: 当表非常大时,即使索引存在,查询优化器可能会选择执行全表扫描,因为扫描整个表可能比使用索引更快。
- 列数据类型不匹配: 当查询条件中的列数据类型与索引列数据类型不匹配时,索引可能会失效。例如,对字符串列应用数值函数。
- 连接操作(JOIN): 在连接操作中,如果没有适用的索引来支持连接条件,索引可能会失效。
- 类型转换: 当查询条件中的列需要进行类型转换以与索引列匹配时,索引通常会失效。
- 子查询: 子查询可能会导致索引失效,尤其是当子查询结果集非常大时。
19、事务四个特性?
- 原子性: 原子性确保事务是不可分割的单元。这意味着事务中的所有操作要么全部成功,要么全部失败。如果在事务执行期间发生故障,系统将撤销(回滚)事务,以确保数据的一致性。
- 一致性: 一致性确保事务将数据库从一个一致性状态转移到另一个一致性状态。这意味着事务执行前后,数据库必须满足所有约束、触发器和规则。如果事务执行导致数据不一致,系统会回滚事务,将数据库恢复到一致性状态。
- 隔离性: 隔离性描述了多个并发事务之间的互相独立性。每个事务应该感觉就像它是在一个独立的环境中运行的,不受其他事务的影响。这可以通过锁定和并发控制来实现,以防止并发事务之间产生干扰。
- 持久性: 持久性确保一旦事务提交,其结果将永久保存在数据库中,即使系统发生故障或崩溃。这通常涉及将事务日志持久化到磁盘。即使在系统重新启动后,事务的结果也应该仍然存在。
20、浅拷贝和深拷贝?
浅拷贝:
- 浅拷贝仅复制对象本身,而不会递归复制其内部包含的对象或数据结构。
- 对于复杂对象,如包含指针或引用的对象,浅拷贝会导致多个对象共享相同的内部数据。
- 浅拷贝通常是通过复制对象的成员变量来实现的,但对于内部指针或引用,只是复制了指针或引用本身,而不是它们引用的对象。
深拷贝:
- 深拷贝会递归地复制对象本身及其内部的所有对象和数据结构,确保每个复制的对象都是全新的,没有共享内部数据。
- 对于复杂对象,深拷贝可以消除共享数据的问题,每个对象都有自己的拷贝。
- 深拷贝需要在拷贝过程中为每个内部对象或数据结构创建全新的副本,通常需要更多的时间和内存。
使用浅拷贝时,多个对象可能会出现数据不一致的问题,因为它们共享相同的内部数据。深拷贝确保了每个对象都是独立的,但可能需要更多的资源。
