数据结构与算法(八):排序算法

news2024/11/18 8:48:41

参考引用

  • Hello 算法
  • Github:hello-algo

1. 选择排序

  • 选择排序的工作原理非常直接:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾,设数组的长度为 n
    • 初始状态下,所有元素未排序,即未排序(索引)区间为 [0, n-1]
    • 选取区间 [0, n-1] 中的最小元素,将其与索引 0 处元素交换。完成后,数组前 1 个元素已排序
    • 选取区间 [1, n-1] 中的最小元素,将其与索引 1 处元素交换。完成后,数组前 2 个元素已排序
    • 以此类推。经过 n-1 轮选择与交换后,数组前 n-1 个元素已排序
    • 仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成
/* 选择排序 */
// 时间复杂度: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. 冒泡排序

  • 冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果 “左元素 > 右元素” 就交换它俩。遍历完成后,最大的元素会被移动到数组的最右端

算法流程

  • 设数组的长度为 n,冒泡排序的步骤如下图所示
    • 首先,对 n 个元素执行 “冒泡”,将数组的最大元素交换至正确位置
    • 接下来,对剩余 n-1 个元素执行 “冒泡”,将第二大元素交换至正确位置
    • 以此类推,经过 n-1 轮 “冒泡” 后,前 n-1 大的元素都被交换至正确位置
    • 仅剩的一个元素必定是最小元素,无须排序,因此数组排序完成

在这里插入图片描述

/* 冒泡排序(标志优化)*/
// 时间复杂度:O(n^2),引入 flag 优化后,最佳时间复杂度可达到 O(n)
// 空间复杂度:O(1)
void bubbleSortWithFlag(vector<int> &nums) {
    // 外循环:未排序区间为 [0, i],控制冒泡排序的轮数
    for (int i = nums.size() - 1; i > 0; i--) {
        bool flag = false; // 初始化标志位,用于标志当前轮次是否有元素交换的标志位
        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                // 交换 nums[j] 与 nums[j + 1]
                // 这里使用了 std::swap() 函数
                swap(nums[j], nums[j + 1]);
                flag = true; // 记录交换元素
            }
        }
        if (!flag)
            break; // 此轮冒泡未交换任何元素,即数组已经是有序的,直接跳出
    }
}

3. 插入排序

  • 插入排序的工作原理与手动整理一副牌的过程非常相似。具体来说,在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置

  • 下图展示了数组插入元素的操作流程。设基准元素为 base ,需要将从目标索引到 base 之间的所有元素向右移动一位,然后再将 base 赋值给目标索引

在这里插入图片描述

3.1 算法流程

  • 初始状态下,数组的第 1 个元素已完成排序
  • 选取数组的第 2 个元素作为 base,将其插入到正确位置后,数组的前 2 个元素已排序
  • 选取第 3 个元素作为 base,将其插入到正确位置后,数组的前 3 个元素已排序
  • 以此类推,在最后一轮中,选取最后一个元素作为 base ,将其插入到正确位置后,所有元素均已排序

在这里插入图片描述

/* 插入排序 */
// 时间复杂度:O(n^2),当输入数组完全有序时为 O(n)
// 空间复杂度: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 赋值到正确位置
    }
}

3.2 插入排序优势

  • 插入排序的时间复杂度为 O(n^2),而快速排序的时间复杂度为 O(nlog n)

    • 尽管插入排序的时间复杂度相比快速排序更高,但在数据量较小的情况下,n^2 和 nlog n 数值接近,插入排序通常更快
  • 实际情况中,插入排序的使用频率显著高于冒泡排序和选择排序

    • 冒泡排序基于元素交换实现,需要借助一个临时变量,共涉及 3 个单元操作;插入排序基于元素赋值实现,仅需 1 个单元操作。因此,冒泡排序的计算开销通常比插入排序更高
    • 选择排序在任何情况下的时间复杂度都为 O(n^2)。如果给定一组部分有序的数据,插入排序通常比选择排序效率更高
    • 选择排序不稳定,无法应用于多级排序

4. 快速排序

  • 快速排序是一种基于分治策略的排序算法
  • 快速排序的核心操作是 “哨兵划分”,其目标是:选择数组中的某个元素作为 “基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧
    • 选取数组最左端元素作为基准数,初始化两个指针 i 和 j 分别指向数组的两端
    • 设置一个循环,在每轮中使用 i(j)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素
    • 循环执行步骤 2,直到 i 和 j 相遇时停止,最后将基准数交换至两个子数组的分界线

    哨兵划分的实质是将一个较长数组的排序问题简化为两个较短数组的排序问题

    • 哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足 “左子数组任意元素 ≤ 基准数 ≤ 右子数组任意元素”。因此,接下来只需对这两个子数组进行排序

在这里插入图片描述

/* 元素交换 */
void swap(vector<int> &nums, int i, int j) {
    int tmp = nums[i];
    nums[i] = nums[j];
    nums[j] = tmp;
}

/* 哨兵划分 */
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, j); // 交换这两个元素
    }
    swap(nums, i, left);  // 将基准数交换至两子数组的分界线
    return i;             // 返回基准数的索引
}

4.1 算法流程

  • 首先,对原数组执行一次 “哨兵划分”,得到未排序的左子数组和右子数组
  • 然后,对左子数组和右子数组分别递归执行 “哨兵划分”
  • 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序

在这里插入图片描述

/* 快速排序 */
// 时间复杂度:O(nlog n),最差情况下 O(n^2)
// 空间复杂度: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);
}

4.2 快排为什么快

  • 快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与 “归并排序” 和 “堆排序” 相同,但通常快速排序的效率更高,主要有以下原因
    • 出现最差情况的概率很低
      • 虽然快速排序的最差时间复杂度为 O(n^2),没有归并排序稳定,但在绝大多数情况下,快速排序能在 O(nlog n) 的时间复杂度下运行
    • 缓存使用效率高
      • 在执行哨兵划分操作时,系统可将整个子数组加载到缓存,因此访问元素的效率较高。而像 “堆排序” 这类算法需要跳跃式访问元素,从而缺乏这一特性
    • 复杂度的常数系数低
      • 在上述三种算法中,快速排序的比较、赋值、交换等操作的总数量最少。这与 “插入排序” 比 “冒泡排序” 更快的原因类似

4.3 基准数优化

  • 快速排序在某些输入下的时间效率可能降低

    • 例如:假设输入数组是完全倒序的,由于选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 n-1、右子数组长度为 0。如此递归下去,每轮哨兵划分后的右子数组长度都为 0,分治策略失效,快速排序退化为 “冒泡排序”
  • 为避免这种情况发生,可以优化哨兵划分中的基准数的选取策略

    • 例如,可以随机选取一个元素作为基准数。然而,如果运气不佳,每次都选到不理想的基准数,效率仍然不尽如人意
    • 可以在数组中选取三个候选元素(通常为数组的首、尾、中点元素),并将这三个候选元素的中位数作为基准数。这样,基准数 “既不太小也不太大” 的概率将大幅提升。还可以选取更多候选元素,以进一步提高算法的稳健性
    /* 选取三个元素的中位数 */
    int medianThree(vector<int> &nums, int left, int mid, int right) {
        // 此处使用异或运算来简化代码
        // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1
        if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right]))
            return left;
        else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right]))
            return mid;
        else
            return right;
    }
    
    /* 哨兵划分(三数取中值) */
    int partition(vector<int> &nums, int left, int right) {
        // 选取三个候选元素的中位数
        int med = medianThree(nums, left, (left + right) / 2, right);
        // 将中位数交换至数组最左端
        swap(nums, left, med);
        // 以 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, j); // 交换这两个元素
        }
        swap(nums, i, left); // 将基准数交换至两子数组的分界线
        return i;            // 返回基准数的索引
    }
    

4.4 尾递归优化

  • 在某些输入下,快速排序可能占用空间较多
    • 以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 0,递归树的高度会达到 n-1,此时需要占用 O(n) 大小的栈帧空间
  • 为了防止栈帧空间的累积,可以在每轮哨兵排序完成后,比较两个子数组的长度
    • 仅对较短的子数组进行递归。由于较短子数组的长度不会超过 n/2,因此这种方法能确保递归深度不超过 log n,从而将最差空间复杂度优化至 O(log n)
    /* 快速排序(尾递归优化) */
    void quickSort(vector<int> &nums, int left, int right) {
        // 子数组长度为 1 时终止
        while (left < right) {
            // 哨兵划分操作
            int pivot = partition(nums, left, right);
            // 对两个子数组中较短的那个执行快排
            if (pivot - left < right - pivot) {
                quickSort(nums, left, pivot - 1); // 递归排序左子数组
                left = pivot + 1;                 // 剩余未排序区间为 [pivot + 1, right]
            } else {
                quickSort(nums, pivot + 1, right); // 递归排序右子数组
                right = pivot - 1;                 // 剩余未排序区间为 [left, pivot - 1]
            }
        }
    }
    

5. 归并排序

  • 归并排序是一种基于分治策略的排序算法,包含下图所示的 “划分” 和 “合并” 阶段
    • 划分阶段:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题
    • 合并阶段:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束

在这里插入图片描述

5.1 算法流程

  • “划分阶段” 从顶至底递归地将数组从中点切分为两个子数组
    • 计算数组中点 mid,递归划分左子数组(区间 [left, mid])和右子数组(区间 [mid + 1, right])
    • 递归执行步骤 1,直至子数组区间长度为 1 时,终止递归划分
  • “合并阶段” 从底至顶地将左子数组和右子数组合并为一个有序数组。需要注意的是,从长度为 1 的子数组开始合并,合并阶段中的每个子数组都是有序的

在这里插入图片描述

  • 归并排序与二叉树后序遍历的递归顺序是一致的
    • 后序遍历:先递归左子树,再递归右子树,最后处理根节点
    • 归并排序:先递归左子数组,再递归右子数组,最后处理合并
    /* 合并左子数组和右子数组 */
    // 左子数组区间 [left, mid]
    // 右子数组区间 [mid + 1, right]
    void merge(vector<int> &nums, int left, int mid, int right) {
        // 初始化辅助数组
        vector<int> tmp(nums.begin() + left, nums.begin() + right + 1);
        // 左子数组的起始索引和结束索引
        int leftStart = left - left, leftEnd = mid - left;
        // 右子数组的起始索引和结束索引
        int rightStart = mid + 1 - left, rightEnd = right - left;
        // i, j 分别指向左子数组、右子数组的首元素
        int i = leftStart, j = rightStart;
        // 通过覆盖原数组 nums 来合并左子数组和右子数组
        for (int k = left; k <= right; k++) {
            // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++
            if (i > leftEnd)
                nums[k] = tmp[j++];
            // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++
            else if (j > rightEnd || tmp[i] <= tmp[j])
                nums[k] = tmp[i++];
            // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++
            else
                nums[k] = tmp[j++];
        }
    }
    
    /* 归并排序 */
    // 时间复杂度:O(nlog n)
    // 空间复杂度: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);
    }
    

merge() 函数注意事项

  • nums 的待合并区间为 [left, right] ,但由于 tmp 仅复制了 nums 该区间的元素,因此 tmp 对应区间为 [0, right - left]
  • 在比较 tmp[i] 和 tmp[j] 的大小时,还需考虑子数组遍历完成后的索引越界问题,即 i > leftEnd 和 j > rightEnd 的情况。索引越界的优先级是最高的,如果左子数组已经被合并完了,那么不需要继续比较,直接合并右子数组元素即可

6. 堆排序

  • 堆排序是一种基于堆数据结构实现的高效排序算法。可利用 “建堆操作” 和 “元素出堆操作” 实现堆排序
    • 输入数组并建立小顶堆,此时最小元素位于堆顶
    • 不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列

    以上方法虽然可行,但需要借助一个额外数组来保存弹出的元素,比较浪费空间

算法流程

  • 1、输入数组并建立大顶堆
    • 完成后,最大元素位于堆顶
  • 2、将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换
    • 完成交换后,堆的长度减 1,已排序元素数量加 1
  • 3、从堆顶元素开始,从顶到底执行堆化操作
    • 完成堆化后,堆的性质得到修复
  • 4、循环执行第 2 和 3 步
    • 循环 n-1 轮后,即可完成数组排序

在这里插入图片描述

/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */
void siftDown(vector<int> &nums, int n, int i) {
    while (true) {
        // 判断节点 i, l, r 中值最大的节点,记为 ma
        int l = 2 * i + 1;
        int r = 2 * i + 2;
        int ma = i;
        if (l < n && nums[l] > nums[ma])
            ma = l;
        if (r < n && nums[r] > nums[ma])
            ma = r;
        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
        if (ma == i) {
            break;
        }
        // 交换两节点
        swap(nums[i], nums[ma]);
        // 循环向下堆化
        i = ma;
    }
}

/* 堆排序 */
// 时间复杂度:O(nlog n)
// 空间复杂度:O(1)
void heapSort(vector<int> &nums) {
    // 建堆操作:堆化除叶节点以外的其他所有节点
    for (int i = nums.size() / 2 - 1; i >= 0; --i) {
        siftDown(nums, nums.size(), i);
    }
    // 从堆中提取最大元素,循环 n-1 轮
    for (int i = nums.size() - 1; i > 0; --i) {
        // 交换根节点与最右叶节点(即交换首元素与尾元素)
        swap(nums[0], nums[i]);
        // 以根节点为起点,从顶至底进行堆化
        siftDown(nums, i, 0);
    }
}

7. 桶排序

  • 前述的几种排序算法都属于 “基于比较的排序算法”,它们通过比较元素间的大小来实现排序。此类排序算法的时间复杂度无法超越 O(nlog n)。接下来,将探讨几种 “非比较排序算法”,它们的时间复杂度可以达到线性阶
  • 桶排序是分治策略的一个典型应用
    • 它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中
    • 然后,在每个桶内部分别执行排序
    • 最终按照桶的顺序将所有数据合并

算法流程

  • 考虑一个长度为 n 的数组,元素是范围 [0, 1) 的浮点数
    • 初始化 k 个桶,将 n 个元素分配到 k 个桶中
    • 对每个桶分别执行排序
    • 按照桶的从小到大的顺序,合并结果

在这里插入图片描述

/* 桶排序 */
// 时间复杂度:O(n + k),最差(所有元素被分至同一个桶中)时间复杂度是 O(n^2)
// 空间复杂度: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;
        }
    }
}

8. 小结

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1072892.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WEEX编译|加密市场三季度回顾及未来展望

作者&#xff1a;Greg Cipolaro&#xff0c;NYDIG 全球研究主管 编译&#xff1a;WEEX 唯客交易所 本文要点&#xff1a; ● 在充满挑战的第三季度&#xff0c;比特币价格下跌 11.1%&#xff0c;因为众多资产类别都在努力应对利率上升的影响和对经济衰退的担忧。 ● 比特币…

接口测试报告的输出

1、对小极客首页跳转接口测试 2、写接口用例 以TestXjkLinks.py命名 1 # --*-- coding:utf-8 --*--2 3 import requests4 import unittest5 import time6 7 8 class TestXjkLinks(unittest.TestCase):9 10 def test_learn(self): 11 u"""首页学习…

Android 13 骁龙相机点击拍照流程分析(一)——点击拍照到更新到左下角缩略图

一.背景 由于最近客户定制需要将文件挂载类型修改为sdcardfs,由于修改了文件挂载类型,导致了骁龙相机拍照后不能点击进入相册,故对骁龙相机从点击事件开始进行问题的排查,此处不介绍最终的sdcardfs挂载后的问题解决方案,有兴趣可以参考jira单:SW2QCM6490-1233 二.流程介…

运行软件提示丢失msvcr120.dll文件怎么办?msvcr120.dll丢失的5个最新解决方法

找不到msvcr120.dll是一个常见的错误信息&#xff0c;通常会在用户尝试运行某些程序或游戏时出现。msvcr120.dll是 Windows 操作系统中的一个动态链接库文件&#xff0c;它包含了 C 运行时库的一些函数和类&#xff0c;对于许多程序和游戏的正常运行至关重要。 当用户遇到找不到…

化妆品用乙基己基甘油全球市场总体规模2023-2029

乙基己基甘油又名辛氧基甘油&#xff0c;分子式 C11H24O3&#xff0c;分子量 204.306&#xff0c;沸点 325℃&#xff0c;密度 0.962&#xff0c;无色液体&#xff0c;涂抹性能适中的润肤剂、保湿剂及润湿剂。它能够在提高配方滋润效果的同时又具有柔滑的肤感。加入在某些膏霜体…

深度学习基础知识 nn.Sequential | nn.ModuleList | nn.ModuleDict

深度学习基础知识 nn.Sequential &#xff5c; nn.ModuleList &#xff5c; nn.ModuleDict 1、nn.Sequential 、 nn.ModuleList 、 nn.ModuleDict 类都继承自 Module 类。2、nn.Sequential、nn.ModuleList 和 nn.ModuleDict语法3、Sequential 、ModuleDict、 ModuleList 的区别…

一文熟练使用python修改Excel中的数据

使用python修改Excel中的内容 1.初级修改 1.1 openpyxl库的功能&#xff1a; openpyxl模块是一个读写Excel 2010文档的Python库&#xff0c;如果要处理更早格式的Excel文档&#xff0c;需要用到额外的库&#xff0c;例如Xlwings。openpyxl是一个比较综合的工具&#xff0c;能…

HarmonyOS/OpenHarmony原生应用-ArkTS万能卡片组件Stack

堆叠容器&#xff0c;子组件按照顺序依次入栈&#xff0c;后一个子组件覆盖前一个子组件。该组件从API Version 7开始支持。可以包含子组件。 一、接口 Stack(value?: { alignContent?: Alignment }) 从API version 9开始&#xff0c;该接口支持在ArkTS卡片中使用。 二、…

缓存雪崩、缓存穿透和缓存击穿产生的原因及解决方案

目录 什么是缓存雪崩&#xff1f; 缓存雪崩的解决方案 什么是缓存穿透&#xff1f; 缓存穿透的解决方案 什么是缓存击穿&#xff1f; 缓存击穿的解决方案 缓存在提高系统性能和响应速度方面起着关键作用&#xff0c;但在实际应用中&#xff0c;我们常常面临一些与缓存相…

关于打造安卓测试机的方法以及常见问题的解决方式

摘要&#xff1a; 本文主要讲解如何打造安卓测试机&#xff0c;并刷机google原生系统、部署Magisk对测试机进行root的常用方式&#xff1b;并对一些常见问题进行思路解答。本文适合新手学习&#xff0c;大佬请绕过 本次实验使用的设备及环境如下&#xff1a; Nexus 5x 测试机…

【多线程进阶】线程安全的集合类

文章目录 前言1. 多线程环境使用 ArrayList2. 多线程环境使用队列3. 多线程环境使用哈希表3.1 HashTable3.2 ConcurrentHashMap 总结 前言 本文主要讲解 Java 线程安全的集合类, 在之前学习过的集合类中, 只有 Vector, Stack, HashTable, 是线程安全的, 因为在他们的关键方法中…

高端品牌如何利用软文抓住顾客的心?

如今高端品市场价值巨大&#xff0c;但之前由于“口罩”影响和冲击&#xff0c;高端品牌的线上销售份额占比较少&#xff0c;同时得益于互联网和新媒体技术的发展&#xff0c;高端品的利润来源大多数是线上推广进行销售&#xff0c;而软文就是高端品常用的推广方式&#xff0c;…

鉴源论坛 · 观模丨基于软件性质的自动化测试技术

作者 | 熊一衡 华东师范大学软件工程学院博士 苏亭 华东师范大学软件工程学院教授 版块 | 鉴源论坛 观模 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 在软件开发的生命周期中&#xff0c;测试是至关重要的一环。为了确保软件产品的质量&#xff0c;开…

MOM与MES管理系统有哪些本质上的区别

随着企业业务的不断发展&#xff0c;许多制造企业开始面临车间管理失控、生产不透明等问题。这时候&#xff0c;很多企业选择上线MES生产管理系统来提高生产管理水平。然而&#xff0c;随着企业业务的不断拓展&#xff0c;MES系统也逐渐暴露出其局限性。于是&#xff0c;MOM平台…

详解CAN通信的标识符掩码和标识符列表两种过滤机制

CAN 通信的应用非常广泛&#xff0c;本文不涉及CAN通信的基础配置&#xff0c;重点分析一下STM32和GD32的CAN通信两种ID过滤方式。 首先&#xff0c;不管是STM32还是GD32&#xff0c;实现CAN通信ID过滤的机制和原理一定是一样的&#xff0c;只是用到的寄存器有差别。 1. ID过…

TensorFlow入门(十四、数据读取机制(1))

TensorFlow的数据读取方式 TensorFlow的数据读取方式共有三种,分别是: ①预加载数据(Preloaded data) 预加载数据的方式,其实就是静态图(Graph)的模式。即将数据直接内嵌到Graph中,再把Graph传入Session中运行。 示例代码如下: import tensorflow.compat.v1 as tf tf.disabl…

超好用的IDEA插件推荐!

大家好&#xff0c;Apipost 最新推出IDEA插件V2版本&#xff01;V2版本主要是Apipost 符合更多用户的需求而推出&#xff0c;支持在插件中获取 token、支持代码完成后在插件中进行 API调试 &#xff0c;同时也保留了1.0版本部分功能如上传选择目录功能等。 V1版本还会继续保留…

韦东山D1S板子——xfel工具无法烧写bin文件到spi norFlash问题解决

1、早期问题排查 &#xff08;1&#xff09;参考博客&#xff1a;《韦东山D1S板子——烧录spi norFlash失败问题排查过程》&#xff1b; &#xff08;2&#xff09;早期排查到xfel工具烧写spi norFlash显示成功&#xff0c;但是实际没有烧写进bin文件&#xff0c;怀疑是norFlas…

“揭秘淘宝店铺所有商品接口:一键获取海量热销宝贝信息!“

淘宝店铺所有商品接口可以通过shop id或店铺主链接获取到整店商品&#xff0c;数据包括&#xff1a;商品ID&#xff0c;图片地址&#xff0c;店铺标题&#xff0c;优惠价&#xff0c;价格&#xff0c;销量&#xff0c;宝贝链接等整个店铺的商品。 要使用这个接口&#xff0c;需…

Linux 系统性能瓶颈分析(超详细)

Author&#xff1a;rab 目录 前言一、性能指标1.1 进程1.1.1 进程定义1.1.2 进程状态1.1.3 进程优先级1.1.4 进程与程序间的关系1.1.5 进程与进程间的关系1.1.6 进程与线程的关系 1.2 内存1.2.1 物理内存与虚拟内存1.2.2 页高速缓存与页写回机制1.2.3 Swap Space 1.3 文件系统1…