[数据结构]-快速排序

news2024/11/26 4:50:38

学习快排的基础

不想自己推导时间复杂度或者了解随机化的合理性,可以忽视下面这一条.

  1. 推导时间复杂度的方法:主定理
  2. 随机化算法—数学基础:概率统计

QuickSort

快速排序:
1962年,Tony Hoare发明了这种如此高效实用的强大排序算法

  1. 分治法的体现:Divide and Conquer
  2. 原地排序,不需要额外申请空间.
  3. 实用.

快速排序分析

快速排序采用了分治思想.
其基本思想:

  1. Divide: 找到一个主元Key,将原数组划分(Partition)成两个子数组Subarray.
    满足左子数组数据小于等于Key,右子树组数据大于Key.
  2. Conquer:递归处理两个子数组,对两个子数组进行快速排序.
  3. Combine:合并-----快速排序不需要这一步,不重要.
    先看第一步:
    Divide:封装一个函数Partition,划分子程序.
    线性时间复杂度: Θ ( n ) \Theta(n) Θ(n)
    对于子数组 A [ p . . . q ] A[p...q] A[p...q]
PARTITION(A, p, q) 
	x = A[p] 
	i = p
	for j = p + 1 to q 
	if A[j] <= x 
		i = i + 1 
		exchange A[i] with A[j] 
exchange A[p] with A[i] 
return i

伪代码能看明白吗?

+------------------------+
|p|  <=X  |i  >x  |j   |q|
+------------------------+

选定一个元素为枢轴,这里先考虑选择数组区间 A [ p . . . q ] A[p...q] A[p...q]的第一个元素A[p].–其它选法暂时别考虑
接下来要用i,j变量来维护区间了,这一过程本质上是将枢轴A[p]放到正确的位置
先看中间过程 [ i . . . j ] [i...j] [i...j]区间,很显然 i i i为分割线–左边<=x,右边>x.
j j j—扫描数据,遍历数组将遇见的元素先判断放到对应区间.
给定[10, 7, 8, 9, 1, 5],自行画图分析一下伪代码的过程吧.
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面我们用简单的python程序实现一下

def swap(A,i,j):
    A[i], A[j] = A[j], A[i]
def Partition(A,p,q):
    x = A[p]
    i = p
    j = i + 1
    while j<q:
        if A[j]<=x:
            i += 1
            swap(A,i,j)
        j+=1
    swap(A,p,i)
    return i

快速排序的伪代码

QuickSort(A, p, q)
    if p < q then
        r = Partition(A, p, q)//
        QuickSort(A, p, r - 1)
        QuickSort(A, r + 1, q)

快速排序只有分,治,没有合并部分.
回忆:归并排序先等分数组,不断递归,最后核心部分是合并两数组.快速排序则是第一步分区间重要,单趟快排原理搞明白了,后面的划分区间递归小菜一碟.
解释上面伪代码:

  1. 递归处理基线问题很重要,弄不好边界,对于特定输出很容易导致栈溢出.
    画出递归树图—p<q时可以递归.
    否则,直接结束调用向上返回,别进递归了,StackOverFlow.
  2. Partition函数要返回分割两区间的下标以便于后续分左右子区间调用快速排序.
  3. r r r分割原区间 [ p , q ] [p,q] [p,q] [ p , r − 1 ] [p,r-1] [p,r1] [ r + 1 , q ] [r+1,q] [r+1,q],递归快排.

还是python代码

def QuickSort(A,p,q):
    if p<q:
        r = Partition(A,p,q)
        QuickSort(A,p,r-1)
        QuickSort(A,r+1,q)

不知道为什么我总有一种不安全感,有一种栈溢出的美.
鲁迅曾经说过:栈分配的内存空间是有限的,递归太深就抛异常了.
不急,先写到快速排序题
直接用C手搓快排,然后提交.

void swap(int *x,int *y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}
int partition(int *nums,int left,int right)
{
    int x = nums[left];
    int i = left;
    for(int j = left + 1;j<right;j++)
    {
        if(nums[j]<x)
        {
            i++;
            swap(nums+i,nums+j);
        }
    }
    swap(nums+left,nums+i);
    return i;
}
void QuickSort(int *nums,int left,int right)
{
    if(left<right)
    {
        int keyi = partition(nums,left,right);
        QuickSort(nums,left,keyi);
        QuickSort(nums,keyi+1,right);
    }
}
int* sortArray(int* nums, int numsSize, int* returnSize) {
    *returnSize = numsSize;
    QuickSort(nums,0,numsSize);
    return nums;
}

竟然超时了:快速排序不是号称时间复杂度 Θ ( n l o g 2 n ) \Theta(nlog_2n) Θ(nlog2n)吗?
对于特定的输入,当给定数组接近有序或者本身有序,快速排序效率降低-原始的快速排序解决不了这种特定输入.
回忆:Insertion Sort(插入排序)—时间复杂度: Θ ( n 2 ) \Theta(n^2) Θ(n2).
快速排序面对这种输入序列,效率和插排一样也是 Θ ( n 2 ) \Theta(n^2) Θ(n2).
不同的是插入排序越接近有序序列,效率越高.而快排则反过来.

# 用这组测试用例测试一下前面的python代码
list = [5]*10000
QuickSort(list,0,len(list))
print(list)

RecursionError: maximum recursion depth exceeded 递归太深,python也抛异常了.
每次快排不是等分的,理想情况每次完美均分,那么画它的递归树就是一棵完全二叉树 Θ ( l o g 2 n ) \Theta(log_2n) Θ(log2n),每趟时间复杂度 Θ ( n ) \Theta(n) Θ(n).两者相乘时间复杂度 Θ ( n l o g 2 n ) \Theta(nlog_2n) Θ(nlog2n).—估计法,此法不严谨.
快排厉害的地方在于每次单趟快排,哪怕不均匀分配,运行效率已然很好.-------稍后会在时间复杂度处给出证明.
若均匀等分时间复杂度 Θ ( l o g 2 n ) \Theta(log_2n) Θ(log2n),那么按照1:9;2:8,3:7划分数组区间这些比例同样很好,即平均来看时间复杂度也是 Θ ( l o g 2 n ) \Theta(log_2n) Θ(log2n).
只有大量单趟快排出现一遍倒的情况,对于这种特定的输入,快排时间复杂度才会退化为上面的糟糕情况.

Java实现朴素快速排序

前面C/Python写了一遍未优化的快速排序.
这种快速排序采用了双指针的算法技巧,我一般称为"前后指针法."
上面"前后指针"的快速排序处理重复元素效率较低,快排创始人霍尔大佬的写法处理重复元素的情况相对较好.

霍尔法

霍尔法还是采用双指针算法
partition函数实现方式不同,其余相同
左右指针(对撞指针):设置两个指针,左指针从左到右遍历,右指针从右到左遍历。
交换元素:

  1. 左指针从左向右移动,直到找到一个大于或等于枢轴的元素。
  2. 右指针从右向左移动,直到找到一个小于或等于枢轴的元素。
  3. 交换这两个元素,并继续移动指针,直到左指针和右指针相撞。
    初学此排序时,最先学的就是霍尔法了,不过此法坑点极多,极容易写错
    请先看代码:
private static int partition(int[] array, int left, int right) {
        //主元下标
        int keyIndex = left;
        int x = array[left];
        while(left<right)
        {
            while(left<right&&array[right]>=x)
            {
                right--;
            }
            //array[right]<x
            while(left<right&&array[left]<=x)
            {
                left++;
            }
            swap(array,left,right);
        }
        swap(array,keyIndex,left);
        return left;
    }
  1. int keyIndex = left;取子数组最左边的数为主元—可见该写法依赖输入序列,是原始的快速排序.
  2. while(left<right)这个最外层循环,对撞指针,循环结束条件left==right
  3. while(left<right&&array[right]>=x) while(left<right&&array[left]<=x)
    为什么内层两循环必须left<right必须存在?由于循环内要调整数组下标,肯定在循环条件上加以限制.否则可能存在数组越界的情况.
    比如:[1,2,3,4,5,6],right->6,它会从右往左找比1小的数,然后一骑绝尘飞出数组数组越界了
  4. 注意6,&&右边均是取了等号的,只保留一个等号或者不取等可以吗?
    假设 while(left<right&&array[right]>x) while(left<right&&array[left]<x).
    两个循环体条件都没去等号,可能发生死循环吗?
    答案是肯定的,这个值就是array[right]==array[left]==x
    举例[3,3,3,3],对于这个数组.left与right均有效,但是它们不会进内部循环.
    后面交换了,还是原先的值依然死循环.
    再假设 while(left<right&&array[right]>x) while(left<right&&array[left]<=x).
    只保留一个取等可以吗?
    答案:也不可以!
    举例:left,right重叠,但right本身没动过,自身交换陷入死循环.
    [110, 100, 0] :right->0,left->110;right不满足移动条件原地不动,left一路右移,与right重叠.执行swap,自己与自己交换没用.再次循环,right不动,left也不动了,至此死循环了.
  5. 为什么先移动right,后移动left?
    首先,请看
    r i g h t : right: right:寻找 < = x <=x <=x的元素.
    l e f t : left: left:寻找 > = x >=x >=x的元素.
    进行交换分区.
    用反证法:
    假设先移动left再移动right.
    无论最终一步是left走还是right走.最终指向位置一定 > = x >=x >=x.最终与x交换,达不到分区的效果.
    举例:[1,3,-1,5,7]left先走,先走到5处,right走到5处就两者相遇,5与x交换.
    数组变为[5,3,-1,1,7],最左边的元素5不满足 < = x <=x <=x,分区失败.
    因此,从这个角度看,right先走非常合理.

以下是Java代码完整实现:

 public static void quickSort(int[] array)
    {
        quick(array,0,array.length-1);
    }
    private static void quick(int[] array,int left,int right)
    {
        if(left<right) {
            int pivot = partition(array, left, right);
            quick(array,left,pivot-1);
            quick(array,pivot+1,right);
        }
    }
    private static void swap(int[] array,int i,int j)
    {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    private static int partition(int[] array, int left, int right) {
        //主元下标
        int keyIndex = left;
        int x = array[left];
        while(left<right)
        {
            while(left<right&&array[right]>=x)
            {
                right--;
            }
            //array[right]<x
            while(left<right&&array[left]<=x)
            {
                left++;
            }
            swap(array,left,right);
        }
        swap(array,keyIndex,left);
        return left;
    }

挖坑法

 private static int partition(int[] array, int left, int right) {
        //先给左边挖个坑
        int key = array[left];
        //记录最初的坑点
        int i = left;
        while(left<right)
        {
            while(left<right&&array[right]>=key)
            {
                right--;
            }
            //右边挖坑填左边
            array[left]=array[right];
            //array[right]<x
            while(left<right&&array[left]<=key)
            {
                left++;
            }
            //左边挖坑填右边
            array[right]=array[left];
        }
        //填最初的坑.
        array[left]=key;
        return left;
    }

前后指针法请自行实现

时间复杂度分析

对于这组[5,5,.....5]-->10000个5的数组列表.面对重复元素,若每次只取第一个元素为枢轴,那么每次单趟遍历一遍数组然后分割数组,分割结果:右数组始终为空数组.
快速排序处理本身有序的数组会非常糟糕,这说明原始的快速排序依赖输入序列.

最坏情况

数组一边倒:对于输入序列[1,2,...n]
T ( n ) = T ( n − 1 ) + T ( 0 ) + n T(n)=T(n-1) + T(0) +n T(n)=T(n1)+T(0)+n
在这里插入图片描述
已知 T ( 0 ) = θ ( 1 ) T(0) = \theta(1) T(0)=θ(1)
θ ( ∑ 0 n c k + T ( 0 ) ) = θ ( n 2 ) \theta(\sum_0^nck+T(0)) =\theta(n^2) θ(0nck+T(0))=θ(n2)
如果一直不均等分配,那么快速排序很低效的.

最佳情况

均匀二等分最佳
T ( n ) = 2 ∗ T ( n / 2 ) + θ ( n ) T(n) = 2*T(n/2) + \theta(n) T(n)=2T(n/2)+θ(n)
符号主定理第二种情况: T ( n ) = θ ( n l g n ) T(n) = \theta(nlgn) T(n)=θ(nlgn)
C S 中 : l g n ☞ l o g 2 n CS中:lgn ☞log_2n CS:lgnlog2n
也可以画递归树.

平均情况

若不均匀分配呢?
比如不是按照最佳5:5比例,而是始终按照1:9比例分配呢?
那么它接近最佳情况还是最糟情况呢?
接近最佳情况:这就是快速排序的魅力时刻
时间复杂度: Θ ( n l g n ) \Theta(nlgn) Θ(nlgn)
证明过程仅靠作者现有工具描述太过粗糙,递归树和放缩法可以粗略得出结论.
详见算法导论书籍以及有关课程

随机化的快速排序

随机化快排是快速排序的优化版本.
严格推导极其依赖概率统计的相关知识,若是有数学基础,请自行查阅算法导论阅读.
快排对于特定序列表现得很糟糕,随机化算法就是想让快速排序不依赖输入序列,快排效率取决随机化的结果.我起初觉得随机化太依赖运气,直到我懵懵地看过推导,才发现其中精妙无穷.

优化方案有很多,先说随机化的三数取中法,然后减少递归深度的快速排序混入插入排序
三数取中法:
每次比较[left...right]与中间下标A[mid],取中位数,返回下标.

private static final int getMiddleNum(int[] arr,int left,int right)
    {
        int mid = (left+right)/2;
        if(arr[left]<arr[right])
        {
            if(arr[mid]<arr[left])
            {
                return left;
            }
            else if(arr[mid]<arr[right])
            {
                return mid;
            }
            else
            {
                return right;
            }
        } else//arr[left]>=arr[right]
        {
            if(arr[mid]<arr[right])
            {
                return right;
            }
            else if(arr[mid]<arr[left])
            {
                return mid;
            }
            else
            {
                return left;
            }
        }
    }
    private static void quick(int[] array,int left,int right)
    {
        if(left<right) {
            int mid = getMiddleNum(array,left,right);
            //将中位数的值与左区间交换.
            swap(array,left,mid);
            int pivot = partition(array, left, right);
            quick(array,left,pivot-1);
            quick(array,pivot+1,right);
        }
    }

插入排序优化
快速排序分而治之,导致后续子数组的数据规模越来越小.换言之,相比原来大的数组,小数组越来越有序了.
面对减小规模且接近有序的数组区间,插入排序算法可以说是最佳选择.
所以我们另起一个分支,设定数据规模,小于等于时采用插入排序,大于则采用递归快速排序.

//插入排序
    private static void insertSort(int[] array,int left ,int right)
    {
        for(int i=left+1;i<=right;i++)
        {
            int tmp=array[i];
            int j = i-1;
            while(j>=0&&array[j]>=tmp)
            {
                array[j+1]=array[j];
                j--;
            }
            array[j+1]=tmp;
        }
    }
 private static final int MAX_LENGTH_INSERT_SORT = 7;//满足插入排序的临界规模
 private static void quick(int[] array,int left,int right) {
        if (left < right) {
            if(right - left + 1<10)
            {
                insertSort(array,left,right);
                return ;
            }
            int mid = getMiddleNum(array, left, right);
            swap(array, left, mid);
            int pivot = partition(array, left, right);
            quick(array, left, pivot - 1);
            quick(array, pivot + 1, right);
        }
    }

可以通过了
不过自己写的快排效率绷不住,感兴趣可以翻一下JavaArrays.sort方法的源码.

补充非递归实现快排.

利用栈的特性.
原因:
栈天然模拟递归.栈的LIFO特性确保了每次处理的子数组顺序与递归调用时保持一致。
画递归树图,你会发现这和递归处理顺序一模一样.
栈:简单,而且利用栈避免递归带来深度过深的栈溢出问题.

下面if(pivot>left+1),你可能感到困惑.
别急,这等同于递归的基线条件:两个及以上元素的区间才能够进行后续操作.
栈存储数对:区间端点下标.将一对数进行入栈操作,进行partition分割.
先处理右边的子数组,再处理左边的子数组:这是栈LIFO的体现.
终止条件:栈为空,那么迭代快排的条件是栈不为空.

    public static void quick2(int[] array,int left,int right)
    {
        Deque<Integer> stack = new ArrayDeque<>();
        int pivot = partition(array,left,right);
        if(pivot>left+1) {
            stack.push(left);
            stack.push(pivot - 1);
        }
        if(pivot<right-1)
        {
            stack.push(pivot+1);
            stack.push(right);
        }
        while(!stack.isEmpty())
        {
            right = stack.pop();
            left = stack.pop();
            pivot = partition(array,left,right);
            if(pivot>left+1) {
                stack.push(left);
                stack.push(pivot - 1);
            }
            if(pivot<right-1)
            {
                stack.push(pivot+1);
                stack.push(right);
            }
        }
    }

结尾

关于快速排序,我觉得自己水平还达不到一点即明.
深入进阶学习,请阅读算法导论或者观看进阶版数据结构.
留待日后提高完善此篇.

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

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

相关文章

数据结构+二叉遍历算法的应用

一、问题描述 编写一个程序&#xff0c;先用二叉树表示一个简单算术表达式&#xff0c;树的每一个 结点包括一个运算符或运算数。在简单算术表达式中只包含、-、 *、/和一位正整数且格式正确(不包含括号)&#xff0c;并且要按照先乘除后 加减的原则构造二叉树。如图 7.35 所示…

QT5生成独立运行的exe文件

目录 1 生成独立运行的exe文件1.1 设置工程Release版本可执行文件存储路径1.2 将工程编译成Release版本 2 使用QT5自带的windeployqt拷贝软件运行依赖项3 将程序打包成一个独立的可执行软件exe4 解决QT5 This application failed to start because no Qt platform plugin could…

批量快递中问题件筛选技巧大揭秘

批量快递中问题件筛选技巧大揭秘&#xff1a;固乔快递查询助手的强大功能 在电商行业日益繁荣的今天&#xff0c;快递服务成为了连接商家与消费者的关键纽带。然而&#xff0c;随着订单量的激增&#xff0c;如何高效处理并筛选出快递中的问题件&#xff0c;成为了许多商家面临…

shuishusihui

互斥量 使用互斥量可以用于保护某些临界资源&#xff0c;同一时间只能有一个任务来使用它。 使用互斥量会引入其他问题&#xff0c;比如说优先级反转&#xff0c;于是提出了优先级继承等方法解决问题 任务通知 任务通知就是通知任务&#xff0c;前边都是多对多的关系&#xff0…

算法解析——双指针算法(3)数据匹配

欢迎来到博主的专栏——算法解析 博主ID&#xff1a;代码小豪 文章目录 LCR179、查找总价格为目标值的两个商品leetcode11——盛水最多的容器leetcode611——有效三角形的个数 我们前面已经盘点了两种双指针算法的使用场景&#xff0c;现在来看看最常见的一种。 通常&#xff0…

社交电商系统开发功能定制

社交电商系统开发功能定制是一项综合性的服务&#xff0c;它涉及到需求分析、界面设计、功能开发、系统测试以及上线后的持续维护。以下是社交电商系统开发中应考虑的关键功能点&#xff1a; 需求分析和规划&#xff1a;开发前&#xff0c;企业需要明确业务需求&#xff0c;包…

【数据分析:一篇文章带你掌握Pandas高级可视化技巧】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本阶段和大家一起分享和探索数据分析的数据可视化&#xff0c;本篇文章主要讲述了&#xff1a;掌握Pandas高级可视化技巧等等。欢迎大家一起探索讨论&#xff01;&#xff01;&#xff01…

docker的swarm技术

docker中swarm技术 docker swarm技术是docker社区提供的docker的集群管理调度工具&#xff0c;通过api来管理多个主机上的docker&#xff0c;通过overlay网络来实现不同主机之间容器的通信与访问。实现容器的调度&#xff0c;资源的分配&#xff0c;以及副本。 docker swarm中…

愛ㄉ粗麵體字體欣赏

愛ㄉ粗麵體字體字体欣赏&#xff0c;英文名AddaNoodle 下载到字体档后&#xff0c;首先打开字体文件预览看看&#xff0c;超可愛的&#xff5e; 最近流行的可愛字體&#xff0c;VIVISTICKER-愛ㄉ粗麵體 QQ 軟軟的筆觸不只適合有溫度的限動 放在貼文或介紹文字都超級適合 還找不…

汇编语言指令 jmp: jmp word ptr、jmp dword ptr、jmp 寄存器、jmp 段地址:偏移地址

1. 转移地址在内存中的jmp指令有2种形式 1.1 jmp word ptr 内存单元地址 jmp word ptr 内存单元地址是段内转移指令&#xff0c;也就是说该指令只修改IP值&#xff0c;其功能是控制CPU下一条执行的指令是一个字&#xff08;2个字节&#xff09;内存中存放的偏移地址所指向的指…

【Python】函数入门(下)

3&#xff09;&#xff09;* ** ​​​​​​注意&#xff1a;也遵循位置传参在前面&#xff0c;按关键字传参在后面。 代码示例&#xff1a; def func(*args,**kwargs):print(args,kwargs) 该函数中的参数会自动根据传参的方式不同&#xff08;即&#xff1a;按位置…

家电行业实施数字工厂管理系统前要做好哪些准备

家电企业在实施数字工厂管理系统前&#xff0c;需要做好以下准备&#xff1a; 一、明确目标与需求 数字化转型目标&#xff1a;明确数字化转型的具体目标&#xff0c;如提高生产效率、降低生产成本、优化库存管理、提升产品质量等。这些目标将指导整个数字工厂管理系统的选择…

STM32学习笔记09-SPI通信

目录 SPI通信简介 硬件电路 移位示意图 SPI基本时序单元 SPI时序 W25Q64简介 硬件电路 W25Q64框图 Flash操作注意事项 SPI外设简介 SPI框图 SPI基本结构 主模式全双工连续传输 非连续传输 软件/硬件波形对比 SPI应用 软件SPI读写W25Q64 硬件SPI读写W25Q64 SP…

在复制 PowerPoint (PPT) 文件时,其中嵌入或者链接的 Excel 文件能够自动创建一个新的实例,而不是继续引用原始的 Excel 文件。

问题&#xff1a; 在复制 PowerPoint (PPT) 文件时&#xff0c;其中嵌入或者链接的 Excel 文件能够自动创建一个新的实例&#xff0c;而不是继续引用原始的 Excel 文件。 解答&#xff1a; 导入已有的excel实测是不行的

为什么程序员应该学习多种语言?

前言 有一句老话适用于所有软件开发人员&#xff1a;知道得越多&#xff0c;成长得越多。程序员学习多种编程语言后、‌可以加速学习新技术、‌拓展认知和视野。‌ 对于程序员来说&#xff0c;‌精通一门编程语言意味着对该语言有深入的理解和熟练的运用&#xff0c;‌这为程…

LLMCompiler执行数学计算的案例

LLMCompiler执行数学计算的案例 LLMCompiler执行数学计算的案例案例代码LLMCompiler的LangGrap结构可视化Tasks DAG可视化完整运行日志 LLMCompiler执行数学计算的案例 这是一个使用LLMCompiler执行数学运算的完整案例&#xff0c;通过设置’ print_dag 参数&#xff0c;可以完…

vme背板 介绍

VME背板是基于VME总线规范的背板,它是VME系统中的重要组成部分,主要负责模块之间的连接和数据传输。VMEbus(Versa Module Eurocard bus)是一种广泛应用于工业控制、军事、航空航天以及电信领域的模块化计算机总线标准。VME背板是VME系统的核心组件之一,它不仅提供了物理支…

思路超清晰的 LVS-NAT 模式实验部署

目录 一、实验原理 1、实验基础配置图 2、实验原理 二、实验环境准备 1、准备四台红帽9的主机 2、四台主机的基础配置 &#xff08;1&#xff09;client 1&#xff09;配置主机名&#xff1a;client 2&#xff09;配置ip:172.25.254.200 &#xff08;2&#xff09;lv…

DAMA学习笔记(十四)-数据管理成熟度评估

1.引言 能力成熟度评估&#xff08;Capability Maturity Assessment&#xff0c;CMA&#xff09;是一种基于能力成熟度模型&#xff08;Capability Maturity Model&#xff0c;CMM&#xff09;框架的能力提升方案&#xff0c;描述了数据管理能力初始状态发展到最优化的过程。CM…

掌握VS Code调试技巧:解决Scrapy模块导入中断问题

介绍 在使用Scrapy进行网页爬取时&#xff0c;Visual Studio Code&#xff08;VS Code&#xff09;作为一款流行的代码编辑器&#xff0c;凭借其强大的调试功能&#xff0c;成为了许多开发者的首选。然而&#xff0c;部分开发者在调试Scrapy爬虫时&#xff0c;常遇到程序在模块…