数据结构与算法1

资料链接 !!!GitHub - krahets/hello-algo: 《Hello 算法》:动画图解、一键运行的数据结构与算法教程,支持 Python, C++, Java, C#, Go, Swift, JS, TS, Dart, Rust, C, Zig 等语言。English edition ongoing

1、基本概念

  • 数据结构是计算机存储、组织数据的方式。
  • 算法是特定问题求解步骤的描述,在计算机中表现为指令的有限序列,是独立存在的一种解决问题的方法和思想。
  • 两者区别:算法是为解决实际问题而设计的;数据结构是算法需要处理问题的载体;数据结构只是静态描述数据元素之间的关系,高效的程序需要在数据结构的基础上设计和选择算法,数据结构与算法相辅相成。

2、算法应用 

2.1 栈的应用

2.1.1  就近匹配——检测字符串中的括号是否成对出现

      字符串:  5+5*(6)+9/3*1)-(1+3(

  • 算法思路如下:
  1. 从第一个字符开始扫描
  2. 当遇见普通字符时忽略,
  3. 当遇见左符号时压入栈中,
  4. 当遇见右符号时从栈中弹出栈顶符号,并进行匹配
    4.1 匹配成功:继续读入下一个字符
    4.2 匹配失败:立即停止,并报错
  5. 扫描结束:
    5.1 成功: 所有字符扫描完毕,且栈为空
    5.2 失败:匹配失败或所有字符扫描完毕但栈非空

2.1.2  中缀表达式与后缀表达式 —— 四则运算

  • 实例
    5 + 4 => 5 4 + 
    1 + 2 * 3 => 1 2 3 * + 
    8 + ( 3 – 1 ) * 5 => 8 3 1 – 5 * +
  • 中缀转后缀算法

         遍历中缀表达式中的数字和符号:

        对于数字,直接输出;
        对于符号:1)左括号:进栈;

                           2)运算符:a)若栈顶符号优先级低,此运算符进栈;

                                                b)若栈顶符号优先级不低,将栈顶符号弹出,然后此运算符进栈。

                           3)右括号:将栈顶符号弹出,直到匹配左括号。

            遍历结束后,将栈中所有符号弹出。   (备注:左括号的优先级最低)

 2.1.3 后缀表达式的计算

        例如:8 3 1 – 5 * +

  • 计算规则如下:
    遍历后缀表达式中的数字和符号:
    对于数字,进栈
    对于符号:从栈中弹出右操作数,从栈中弹出左操作数,根据符号进行运算,将运算结果                         入栈。
    遍历结束:栈中的唯一数字为最终计算结果。

2.2 堆的应用 

 堆(Heap)是一种满足特定条件的完全二叉树,主要分为两种类型:

  • 小顶堆(min heap):任意节点的值 <= 其子节点的值。
  • 大顶堆(max heap):任意节点的值 >= 其子节点的值。

vector<int> input{1,2,3,4,5};
//大顶堆
priority_queue<int, vector<int>, less<int>>maxHeap(input.begin(),input.end());

//小顶堆
priority_queue<int, vector<int>, greater<int>>minHeap;
minHeap.push(1);

 2.2.1 Top-k问题

  基于堆更加高效地解决 Top-k 问题,流程如下所示。

  1. 初始化一个小顶堆,其堆顶元素最小。
  2. 先将数组的前 k 个元素依次入堆。
  3. 从第 k+1 个元素开始,若当前元素大于堆顶元素,则将堆顶元素出堆,并将当前元素入堆。
  4. 遍历完成后,堆中保存的就是最大的 k个元素。
/* 基于堆查找数组中最大的 k 个元素 */
priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {
    // 初始化小顶堆
    priority_queue<int, vector<int>, greater<int>> heap;
    // 将数组的前 k 个元素入堆
    for (int i = 0; i < k; i++) {
        heap.push(nums[i]);
    }
    // 从第 k+1 个元素开始,保持堆的长度为 k
    for (int i = k; i < nums.size(); i++) {
        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆
        if (nums[i] > heap.top()) {
            heap.pop();
            heap.push(nums[i]);
        }
    }
    return heap;
}

2.3 搜索

2.3.1 二分查找法(binary search)

(时间复杂度O(logn),空间复杂度O(1))

/* 二分查找(双闭区间) */
int binarySearch(vector<int> &nums, int target) {
    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
    int i = 0, j = nums.size() - 1;
    // 循环,当搜索区间为空时跳出(当 i > j 时为空)
    while (i <= j) {
        int m = i + (j - i) / 2; // 计算中点索引 m
        if (nums[m] < target)    // 此情况说明 target 在区间 [m+1, j] 中
            i = m + 1;
        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中
            j = m - 1;
        else // 找到目标元素,返回其索引
            return m;
    }
    // 未找到目标元素,返回 -1
    return -1;
}

 2.3.2 哈希优化策略

通过将线性查找替换为哈希查找来降低算法的时间复杂度。

Q: 给定一个整数数组 nums 和一个目标元素 target ,请在数组中搜索“和”为 target 的两个元素,并返回它们的数组索引。返回任意一个解即可。

A1: 线性查找:以时间换空间(时间复杂度O(n^{2}),空间复杂度O(1))

/* 方法一:暴力枚举 */
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
    int size = nums.size();
    // 两层循环,时间复杂度为 O(n^2)
    for (int i = 0; i < size - 1; i++) {
        for (int j = i + 1; j < size; j++) {
            if (nums[i] + nums[j] == target)
                return {i, j};
        }
    }
    return {};
}

A2:哈希查找:以空间换时间(时间复杂度O(^{}n),空间复杂度O(^{}n))

/* 方法二:辅助哈希表 */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
    int size = nums.size();
    // 辅助哈希表,空间复杂度为 O(n)
    unordered_map<int, int> dic;
    // 单层循环,时间复杂度为 O(n)
    for (int i = 0; i < size; i++) {
        if (dic.find(target - nums[i]) != dic.end()) {
            return {dic[target - nums[i]], i};
        }
        dic.emplace(nums[i], i);
    }
    return {};
}

2.4 排序

2.4.1 选择排序(selection sort) 

        工作原理:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。   (时间复杂度O(n^{2}),空间复杂度O(1))

/* 选择排序 */
void selectionSort(vector<int> &nums) {
    int n = nums.size();
    // 外循环:未排序区间为 [i, n-1]
    for (int i = 0; i < n - 1; i++) {
        // 内循环:找到未排序区间内的最小元素
        int k = i;
        for (int j = i + 1; j < n; j++) {
            if (nums[j] < nums[k])
                k = j; // 记录最小元素的索引
        }
        // 将该最小元素与未排序区间的首个元素交换
        swap(nums[i], nums[k]);
    }
}

2.4.2 冒泡排序(bubble sort)

         通过连续地比较与交换相邻元素实现排序。冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换二者。遍历完成后,最大的元素会被移动到数组的最右端。  (时间复杂度O(n^{2}),空间复杂度O(1))

/* 冒泡排序 */
void bubbleSort(vector<int> &nums) {
    // 外循环:未排序区间为 [0, i]
    for (int i = nums.size() - 1; i > 0; i--) {
        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                swap(nums[j], nums[j + 1]);
            }
        }
    }
}

 2.4.3 插入排序(insertion sort)

         在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。  (时间复杂度O(n^{2}),空间复杂度O(1))

/* 插入排序 */
void insertionSort(vector<int> &nums) {
    // 外循环:已排序元素数量为 1, 2, ..., n
    for (int i = 1; i < nums.size(); i++) {
        int base = nums[i], j = i - 1;
        // 内循环:将 base 插入到已排序部分的正确位置
        while (j >= 0 && nums[j] > base) {
            nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位
            j--;
        }
        nums[j + 1] = base; // 将 base 赋值到正确位置
    }
}

2.4.4 快速排序(quick sort)

         基于分治策略,快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。

(时间复杂度O(nlogn),空间复杂度O(n))

/* 快速排序 */
void quickSort(vector<int> &nums, int left, int right) {
    // 子数组长度为 1 时终止递归
    if (left >= right)
        return;
    // 哨兵划分
    int pivot = partition(nums, left, right);
    // 递归左子数组、右子数组
    quickSort(nums, left, pivot - 1);
    quickSort(nums, pivot + 1, right);
}

/* 哨兵划分 */
int partition(vector<int> &nums, int left, int right) {
    // 以 nums[left] 为基准数
    int i = left, j = right;
    while (i < j) {
        while (i < j && nums[j] >= nums[left])
            j--;                 // 从右向左找首个小于基准数的元素
        while (i < j && nums[i] <= nums[left])
            i++;                 // 从左向右找首个大于基准数的元素
        swap(nums[i], nums[j]);  // 交换这两个元素
    }
    swap(nums[i], nums[left]);   // 将基准数交换至两子数组的分界线
    return i;                    // 返回基准数的索引
}

2.4.5 归并排序(merge sort) 

        基于分治策略,包含如下两个阶段:

  • 划分阶段:通过递归不断地将数组从中点处分开,将长数组排序问题转换为短数组排序问题。
  • 合并阶段:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。

(时间复杂度O(nlogn),空间复杂度O(n)) 

/* 归并排序 */
void mergeSort(vector<int> &nums, int left, int right) {
    // 终止条件
    if (left >= right)
        return; // 当子数组长度为 1 时终止递归
    // 划分阶段
    int mid = (left + right) / 2;    // 计算中点
    mergeSort(nums, left, mid);      // 递归左子数组
    mergeSort(nums, mid + 1, right); // 递归右子数组
    // 合并阶段
    merge(nums, left, mid, right);
}

/* 合并左子数组和右子数组 */
void merge(vector<int> &nums, int left, int mid, int right) {
    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]
    // 创建一个临时数组 tmp ,用于存放合并后的结果
    vector<int> tmp(right - left + 1);
    // 初始化左子数组和右子数组的起始索引
    int i = left, j = mid + 1, k = 0;
    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j])
            tmp[k++] = nums[i++];
        else
            tmp[k++] = nums[j++];
    }
    // 将左子数组和右子数组的剩余元素复制到临时数组中
    while (i <= mid) {
        tmp[k++] = nums[i++];
    }
    while (j <= right) {
        tmp[k++] = nums[j++];
    }
    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间
    for (k = 0; k < tmp.size(); k++) {
        nums[left + k] = tmp[k];
    }
}

2.4.6 堆排序(heap sort) 

        基于堆数据结构实现。(priority_queue<int, vector<int>, greater<int>>minHeap)

可以利用“建堆操作”和“元素出堆操作”实现堆排序。

  • 输入数组并建立小顶堆,此时最小元素位于堆顶。
  • 不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列。

以上方法虽然可行,但需要借助一个额外数组来保存弹出的元素,比较浪费空间。在实际中,我们通常使用一种更加优雅的实现方式。

2.4.7 桶排序(bucket sort)

         基于分治策略,通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。

考虑一个长度为^{}n 的数组,其元素是范围 [0,1) 内的浮点数。桶排序的流程如下所示:

  1. 初始化 ^{}k 个桶,将 ^{}n 个元素分配到 ^{}k 个桶中。
  2. 对每个桶分别执行排序(采用内置排序函数sort)。
  3. 按照桶从小到大的顺序合并结果。

(时间复杂度O(n+k),空间复杂度O(n+k)) 

/* 桶排序 */
void bucketSort(vector<float> &nums) {
    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素
    int k = nums.size() / 2;
    vector<vector<float>> buckets(k);
    // 1. 将数组元素分配到各个桶中
    for (float num : nums) {
        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]
        int i = num * k;
        // 将 num 添加进桶 bucket_idx
        buckets[i].push_back(num);
    }
    // 2. 对各个桶执行排序
    for (vector<float> &bucket : buckets) {
        // 使用内置排序函数,也可以替换成其他排序算法
        sort(bucket.begin(), bucket.end());
    }
    // 3. 遍历桶合并结果
    int i = 0;
    for (vector<float> &bucket : buckets) {
        for (float num : bucket) {
            nums[i++] = num;
        }
    }
}

2.4.8 计数排序(counting sort)

2.4.9 基数排序(radix sort)

排序算法对比

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值