【数据结构】 面试复习

1. C++

2. 计算机网络

3. 操作系统

4. 数据库

5. 数据结构

6. 杂项

1. 红黑树与AVL树

1.1 性质

5个属性:color, key, left, right, p
(能保证在最坏情况下,基本的动态几何操作的时间均为O(lgn))

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 每个叶子节点都是黑色的空节点(NIL节点)。
  4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。(红黑树从根到叶节点的最长路径都不会超过最短路径的两倍

红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。

  • BST & RB-T: 相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)(只有左子树或者右子树,退化为链表)。

  • AVL & RB-T: 在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。两个都为O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。插入多用红黑树,插入少用avl

1.2 应用

主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。

1.3 旋转

在这里插入图片描述
左旋
在这里插入图片描述
右旋代码类似。
时间复杂度:O(1)

1.4 插入

每次插入位置都是叶节点,
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
时间复杂度:O(lgn)

1.5 删除

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
时间复杂度:O(lgn)

2. B树与B+树

2.1 性质

B树:关键字分布在整颗树中,M阶B树

  1. 非叶节点最多有m棵子树,
  2. 根结点最少有两个子树,非根非叶节点最少有m/2棵子树
  3. 非叶子结点中保存的关键字,等于该结点子树指针的个数-1
  4. 关键字大小有序,关键字的左子树都小于该关键字,右子树都大于该关键字
  5. 叶子结点都在同一层
    在这里插入图片描述
    在这里插入图片描述

2.2 操作

插入 删除 分裂节点

2.3 B+树:

  1. 非叶子结点的子树指针对关键字个数相同
  2. 非叶子结点的子树子树指针p[i]指向关键字区间[ k[i],k[i+1] )开区间(关键字是子树最小)
  3. 叶子结点增加一个链指针
  4. 非叶子结点只是个索引,所有关键字都在叶子结点出现

3. 排序

3.1 冒泡排序

  1. 如果遇到相等的值不进行交换,那这种排序方式是稳定的排序方式。

1. 原理: 比较两个相邻的元素,将值大的元素交换到右边

2. 思路: 依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。

(1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。

(2) 比较第2和第3个数,将小数 放在前面,大数放在后面。

(3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成

(4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。

(5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。

(6)依次类推,每一趟比较次数减少依次
3. 举例    
    在这里插入图片描述
4. 代码实现

void BubbleSort(int arr[], int a){
    if(arr == NULL || a < 0) return;
    int temp;
    for(int i = 0; i < a; i++){
        for(int j = i + 1; j < a; j++){
            if(arr[i] > arr[j]){
                temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }
    print(arr, a);
}

3.2 选择排序

1. 概念 选择排序(Select Sort)是直观的排序,通过确定一个 Key 最大或最小值,再从带排序的的数中找出最大或最小的交换到对应位置。再选择次之。双重循环时间复杂度为 O(n^2)

2. 算法描述:
在一个长度为 N 的无序数组中,第一次遍历 n-1 个数找到最小的和第一个数交换。
第二次从下一个数开始遍历 n-2 个数,找到最小的数和第二个数交换。
重复以上操作直到第 n-1 次遍历最小的数和第 n-1 个数交换,排序完成。
3. 代码实现

void SelectSort(int arr[], int a){
    if(arr == NULL || a <= 0) return;
    for(int i = 0; i < a; i++){
        int MinKey = arr[i];
        for(int j = i; j < a; j++){
            if(MinKey > arr[j]){
                int temp = arr[j];
                arr[j] = MinKey;
                MinKey = temp;
            }
        }
        arr[i] = MinKey;
    }
}

3.3 插入排序

1. 实现思路:

  1. 从数组的第二个数据开始往前比较,即一开始用第二个数和他前面的一个比较,如果 符合条件(比前面的大或者小,自定义),则让他们交换位置。
  2. 然后再用第三个数和第二个比较,符合则交换,但是此处还得继续往前比较,比如有 5个数8,15,20,45, 17,17比45小,需要交换,但是17也比20小,也要交换,当不需 要和15交换以后,说明也不需要和15前面的数据比较了,肯定不需要交换,因为前 面的数据都是有序的。
  3. 重复步骤二,一直到数据全都排完。
    2. 代码实现
void InsertSort(int a[], int n){  //直接插入排序
    int i,j,temp=0;
    for(i=1;i<n;i++){
        if(a[i]<a[i-1]){
            temp = a[i];
            for(j=i-1;j>=0 && a[j]>temp;j--) {
                a[j+1]=a[j];
            }
            a[j+1]=temp;
        }
    }
}

3.4 快排

1. 基本概念
快速排序(Quick Sort)使用分治法策略。它的基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

2. 算法流程:

  • (1) 从数列中挑出一个基准值。
  • (2) 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
  • (3) 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。
    3. 图解

在这里插入图片描述
4. 随机快排
经典快速排序总是指定数组或者某部分的最后一个元素作为基准值,随机快速排序指定数组或者某一部分中的随机值作为基准值。快排随机版本只需要 先使用rand()方法找到一个随机下标,接着swap(nums[left], nums[i]),然后和普通版本代码一模一样

5. 代码实现

LeetCode 215.数组中的第k个最大元素

int findKthLargestQuick(vector<int>& nums, int k, int left, int right){
    // 快排思想
    int n = nums.size();
    
    // 随机版本
    int i = rand() % (right - left + 1) + left;
    swap(nums[left], nums[i]);
    
    int l = left, r = right, mark = nums[left];
    while(l < r){
        while(l < r && nums[r] >= mark) r--;
        while(l < r && nums[l] <= mark) l++;
        swap(nums[l], nums[r]);
    }
    swap(nums[left], nums[l]);
    if(right - l + 1 < k) return findKthLargestQuick(nums, k - (right - l + 1), left, l - 1);
    else if(right - l + 1 == k) return nums[l];
    else return findKthLargestQuick(nums, k , l + 1, right);
}

3.5 堆排序

1. 堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
在这里插入图片描述
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
在这里插入图片描述
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
2. 算法思想
 堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
3. 算法流程(以大顶堆为例)

  1. 堆调整maxHeapify
    在这里插入图片描述
    在这里插入图片描述
 void maxHeapify(vector<int>& nums, int i, int heapSize){
        int l = i * 2 + 1, r = i * 2 + 2, largest = i;
        // 找到最大的一个元素
        if(l < heapSize && nums[l] > nums[i]) largest = l;
        if(r < heapSize && nums[r] > nums[largest]) largest = r;
        // nums[i]符合堆要求,然后递归调整堆
        if(largest != i){
            swap(nums[largest], nums[i]);
            maxHeapify(nums, largest, heapSize);
        } 
    }
  1. 建堆
    在这里插入图片描述
    举例
    在这里插入图片描述
// nums(n/2+1..n)都是叶节点,可以看做已经满足了堆的性质,无需调整
void buildMaxHeap(vector<int>& nums, int heapSize){
    for(int i = heapSize / 2; i >= 0; i--){
        maxHeapify(nums, i, heapSize);
    }
}
  1. 堆排序
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    4. 题目举例
    LeetCode 215.数组中的第k个最大元素
    buildMaxHeap 初始构建堆
    maxHeapfy 堆调整
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 一、快排
        // return findKthLargestHelp(nums, k, 0, nums.size() - 1);
        // 二、堆排
        // 1. 构建size = nums.size()的大根堆,删除size - k + 1次,最后一个删除的
        // 2. 构建size = k的小根堆,构建n次,最后一个堆的根
        // return findKthLargestMaxHeap(nums, k);
        return findKthLargestMinHeap(nums, k);
    }

    // quickSort 快排
    int findKthLargestQuick(vector<int>& nums, int k, int left, int right){
        // 快排思想
        int n = nums.size();
        int i = rand() % (right - left + 1) + left;
        swap(nums[left], nums[i]);
        int l = left, r = right, mark = nums[left];
        while(l < r){
            while(l < r && nums[r] >= mark) r--;
            while(l < r && nums[l] <= mark) l++;
            swap(nums[l], nums[r]);
        }
        swap(nums[left], nums[l]);
        if(right - l + 1 < k) return findKthLargestQuick(nums, k - (right - l + 1), left, l - 1);
        else if(right - l + 1 == k) return nums[l];
        else return findKthLargestQuick(nums, k , l + 1, right);
    }
    
  	// maxheap 大顶堆解决
    int findKthLargestMaxHeap(vector<int>& nums, int k){
        int heapSize = nums.size();
        buildMaxHeap(nums, heapSize);
        for(int i = nums.size() - 1; i >= nums.size() - k + 1; i--){
            swap(nums[i], nums[0]);
            --heapSize;
            maxHeapify(nums, 0, heapSize);
        }
        return nums[0];
    }
    // 构建大顶堆
    // nums(n/2+1..n)都是叶节点,可以看做已经满足了堆的性质,无需调整
    void buildMaxHeap(vector<int>& nums, int heapSize){
        for(int i = heapSize / 2; i >= 0; i--){
            maxHeapify(nums, i, heapSize);
        }
    }
    // 堆调整
    void maxHeapify(vector<int>& nums, int i, int heapSize){
        int l = i * 2 + 1, r = i * 2 + 2, largest = i;
        // 找到最大的一个元素
        if(l < heapSize && nums[l] > nums[i]) largest = l;
        if(r < heapSize && nums[r] > nums[largest]) largest = r;
        // nums[i]符合堆要求,然后递归调整堆
        if(largest != i){
            swap(nums[largest], nums[i]);
            maxHeapify(nums, largest, heapSize);
        } 
    }
    
    // minHeap 小顶堆解决
    int findKthLargestMinHeap(vector<int>& nums, int k){
        int heapSize = k;
        // 构建k大小的小根堆
        for(int i = heapSize / 2; i >= 0; i--){
            minHeapify(nums, i, heapSize);
        }
        for(int i = k; i < nums.size(); i++){
            if(nums[0] < nums[i]){
                swap(nums[0], nums[i]);
                minHeapify(nums, 0, heapSize);
            }
        }
        return nums[0];
    }
    void minHeapify(vector<int>& nums, int i, int heapSize){
        int l = i * 2 + 1, r = i * 2 + 2, smallest = i;
        if(l < heapSize && nums[l] < nums[i]) smallest = l;
        if(r < heapSize && nums[r] < nums[smallest]) smallest = r;
        if(smallest != i){
            swap(nums[smallest], nums[i]);
            minHeapify(nums, smallest, heapSize);
        }
    }
};

3.6 归并排序

1. 基本概念
**归并排序(MERGE-SORT)**是建立在归并操作上的一种有效的排序算法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之),将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序,若将两个有序表合并成一个有序表,称为二路归并。
归并排序其实要做两件事:

  • (1)“分解”——将序列每次折半划分(递归实现)
  • (2)“合并”——将划分后的序列段两两合并后排序
    2. 基本思想
    将待排序序列R[0…n-1]看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序表;将这些有序序列再次归并,得到n/4个长度为4的有序序列;如此反复进行下去,最后得到一个长度为n的有序序列
    3.算法描述
  • 第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  • 第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
  • 第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  • 重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾
    4. 图解
    在这里插入图片描述
    5. 代码实现
/* 将序列对半拆分直到序列长度为1*/
void MergeSort_UptoDown(int *num, int start, int end){
    int mid = start + (end - start) / 2;
    if (start >= end) return;
    MergeSort_UptoDown(num, start, mid);
    MergeSort_UptoDown(num, mid + 1, end);
    Merge(num, start, mid, end);
}

void Merge(int *num, int start, int mid, int end){
    int *temp = (int *)malloc((end-start+1) * sizeof(int));    //申请空间来存放两个有序区归并后的临时区域
    int i = start;
    int j = mid + 1;
    int k = 0;
    while (i <= mid && j <= end) {
        if (num[i] <= num[j]) temp[k++] = num[i++];
        else temp[k++] = num[j++];
    }
    while (i <= mid)
        temp[k++] = num[i++];
    while (j <= end)
        temp[k++] = num[j++];

    //将临时区域中排序后的元素,整合到原数组中
    for (i = 0; i < k; i++)
        num[start + i] = temp[i];
    free(temp);
}

3.7 总结

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值