力扣详解(75颜色分类)

本文详细探讨了如何通过三指针、双指针(包括一种左闭右开和一种首尾双向)和单指针的方法解决LeetCode 75题的颜色排序问题。讲解了每种解题思路的关键步骤,并揭示了常见陷阱。同时,文中提到了几种排序算法的简化版本,如冒泡排序、选择排序和插入排序,但未涉及归并排序、计数排序和快速排序的错误代码。
摘要由CSDN通过智能技术生成

一、题目

略,请见力扣 75 题。

二、题解

(一)为了解题而解题

(1)三指针一次循环
可以看作有三个区间:全 0 区间、全 1 区间、全 2 区间。
定义三个变量指向(标记)三个区间末尾的下一个位置,看图:
在这里插入图片描述
为什么这么安排,先看代码再看几个主要步骤:

class Solution {
public:
    void sortColors(vector<int>& nums) {
        //0,1,2的指针
        int nums0 = 0,nums1 = 0,nums2 = 0;
        //数组大小
        int n = nums.size();
        //遍历计数器
        int i = 0;
        while(i <= (n - 1))
        {
            if(nums[i] == 0)
            {
                nums[nums2++] = 2;
                nums[nums1++] = 1;
                nums[nums0++] = 0;
            }
            else if(nums[i] == 1)
            {
                nums[nums2++] = 2;
                nums[nums1++] = 1;
            }
            else
            {
                nums[nums2++] = 2;
            }
            ++i;
        }
    }
};

①假设刚开始有如下数组:
在这里插入图片描述
②第一次 while 循环,执行 else 语句:
在这里插入图片描述
③第二次 while 循环,执行 else if 语句:
在这里插入图片描述
在这里插入图片描述
④第三次 while 循环,执行 else 语句:
在这里插入图片描述
⑤第四次 while 循环,执行 if 语句:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
到这里就基本能了解了。
(2)双指针一次循环(第一种)
根据题目可以知道,最终排好后的数组是 0 全部在数组首部,1 全部在数组中间,2全部在数组尾部。
所以定义一个遍历计数器,再定义两个变量 left 、 right 分别指向数组首尾,计数器往后走,当计数器所在位置元素为 0 则将该数与 left 当前所在位置的元素交换,然后 left 往后走,同理如果计数器所在位置是 2 则将该数与 right 当前所在位置元素交换,然后 right 往前走,直到 i > right 结束循环,其中 0 和 2 排好后,1 自然排到正确的位置。
先看代码再看几个主要步骤:

class Solution {
public:
    void sortColors(vector<int>& nums) {
        //数组长度
        int n = nums.size();
        //首尾双指针
        int left = 0,right = n - 1;
        //遍历计数器
        int i = 0;
        while(i <= right)
        {
            if(nums[i] == 0)
            {
                swap(nums[left++],nums[i++]);//交换后都往后走
            }
            else if(nums[i] == 2)
            {
                swap(nums[right--],nums[i]);
            }
            else  ++i;//为 1 直接下一次排序
        }
    }
private:
    void swap(int &a,int &b)
    {
        int tmp = a;
        a = b;
        b = tmp;
    }
};

有几个细节地方要注意一下,也是我绊倒的地方:

问:循环条件不能是 left < right?
答:有一种情况,比如 10 个数的数组中,只有 2 个 0,5 个 1,3 个 2,头脑快的可以直接想到 left 和 right 永远不会牵手,所有的 0 和 2 分别交换到数组首部和尾部后,left = 2,right = 6,头脑没那么快的看图:

①刚开始:
在这里插入图片描述
②中间省略一点步骤,直接到最后一个 2 被交换到数组尾部后的情况,此时 right = 6,可见所有 2 都被交换到了数组尾部,所以 right 不会再移动:
在这里插入图片描述
③中间省略一点步骤,直接到最后一个 0 被交换到数组首部后的情况,可见所有 0 都已经交换到数组首部,所以 left 不会再移动,而 left 依旧小于 right,i 也就会继续往后走,但是现在数组已经有序,所以就会出现错误:
在这里插入图片描述
④如其中一个错误:
在这里插入图片描述
此时 nums[i] = 2,会执行 else if 语句,最终导致错误
在这里插入图片描述

问:else 语句可不可以放到最前面?
答:不可以,如果你当前 i 的位置的后面连续几个数刚好是 1 ,0,2,则会出错。看图:

①刚开始:
在这里插入图片描述
②第一次循环后:
在这里插入图片描述
③第二次循环就满足了1,0,2 的情况,则三个语句都会执行:
第一个 if 条件满足
在这里插入图片描述
并且 else if 条件满足
在这里插入图片描述
离谱的是 else 也满足了,所以出错了
在这里插入图片描述
④第三次循环条件不成立,跳出,得出错误结果

问:循环条件 i <= right 不是多此一举吗?
答:因为 right 指向的是全 2 区间的前一个位置,不在全 2 区间里,也就是说 right 指向的位置的数是没有排序的,所以 i 要走到和 right 同一个位置才算完整。

①刚开始:
在这里插入图片描述
②第一次循环:
在这里插入图片描述
③第二次循环,此时 i = right 不满足 i < right,循环结束,结果错误。

问:else if 语句中 swap 中的 nums[i] 不可以写成 nums[i++] 吗?
答:不可以,因为你不能保证的当前交换的 right 位置的值就是 0 或者 1,万一是 2,而你 i++,相当于下一次循环就是后面一个位置的数了,从而出错。看图:

①刚开始:
在这里插入图片描述
②中间省略一点,直接到第二次循环:
在这里插入图片描述
执行 else if 语句,交换后
在这里插入图片描述
③第三次循环,也就是最后一次循环,得到的结果显然不正确:
在这里插入图片描述
(3)双指针一次循环(第二种)
同样,定义两个标记变量(指针)和一个遍历计数器,不同于第一种的是,两个指针不是一首一尾,而是都从数组首部开始往后走,分别标记全 0 区间和全 1 区间末尾的后一位置。不明白?
先看代码再看几个关键图:

class Solution {
public:
    void sortColors(vector<int>& nums) {
        //双定位变量
        int p0 = 0,p1 = 0;
        //遍历计数器
        int i;
        for(i = 0;i < nums.size();++i)
        {
            if(nums[i] == 1)
            {
                swap(nums[i],nums[p1]);
                ++p1;
            }
            if(nums[i] == 0)
            {
                swap(nums[i],nums[p0]);
                if(p0 < p1)  swap(nums[i],nums[p1]);
                ++p0;
                ++p1;
            }
        }
    }
private:
    void swap(int &a,int &b)
    {
        int tmp = a;
        a = b;
        b = tmp;
        return;
    }
};

①刚开始:
在这里插入图片描述
②省略第一次循环,第二次循环,执行第二个 if 语句:
在这里插入图片描述
执行第一个 swap
在这里插入图片描述
不满足内层 if 的条件,不执行
在这里插入图片描述
③中间省略一点步骤,来到这里
在这里插入图片描述
执行第一个 if 语句,此时可以看到两个指针都是分别指向对应区间末尾后一个位置
在这里插入图片描述
④依次类推,来到这里,可以看到会执行第二个 if 语句:
在这里插入图片描述
执行第一个 swap 后,可以看到结果并不对,因为最后一个 1 的前面还有两个 2,这也就是内层那个 if 语句的作用了,因为左闭右开的区间 [ p0,p1 ) 内全部是 1,也就是 p0 < p1 就说明 [ p0,p1)区间内至少有一个 1,而下一次遇到 0 时,i 位置的 0 会与 p0 位置的 1 交换,将 1 交换到 i 的位置,但是你不能保证 (p1,i)区间内没有 2
在这里插入图片描述
⑤最终结果:
在这里插入图片描述
(4)单指针两次循环
思路非常简单,就是定义一个标记变量(指针)和一个遍历计数器,进行两次循环分别交换 0 和 1,最后 2 自然有序。小白都懂,不多说。
直接代码,我也不再作图演示:

class Solution {
public:
    void sortColors(vector<int>& nums) {
        //单指针
        int ptr;
        //遍历计数器
        int i;
        //数组长度
        int n = nums.size();
        for(i = 0,ptr = 0;i < n;++i)
            if(nums[i] == 0)
            {
                swap(nums[i],nums[ptr]);
                ++ptr;
            }
        //注意 ptr 不要再赋值为 0,因为你是排好 0 后再排 1 ,所以初始状态 ptr 应该是全 0 区间尾部后一个位置
        for(i = 0;i < n;++i)
            if(nums[i] == 1)
            {
                swap(nums[i],nums[ptr]);
                ++ptr;
            }
    }
private:
    void swap(int &a,int &b)
    {
        int tmp = a;
        a = b;
        b = tmp;
    }
};

(二)排序算法解题

因为能力尚且不足,所以其中循环归并、计数排序、快速排序都没有写出来(有错误)。
如果需要的可以看正确的几个排序算法代码和上面的为了解题而解题代码,如果有大佬,也可以帮我纠正纠正这几个代码,先行感谢,等我学成归来就会把文章尚错误的地方改过来。

#include<iostream>
using namespace std;
#define N 100
class Solution 
{
public:
    void sortColors_bubble();//冒泡排序,用时 4 ms,空间 8.1 MB(swap函数直接写在冒泡函数里 8 MB)
    void sortColors_selection();//选择排序,用时 4 ms,空间 7.9 MB
    void sortColors_insertion();//插入排序,用时 4 ms,空间 7.9 MB
    void sortColors_mergemain();//归并排序主函数
    void sortColors_shellmain();//希尔排序,用时 4 ms,空间 8 MB
    void sortColors_counting();//计数排序
    void sortColors_quickmain();//快速排序主函数
    void output();//输出数据
    Solution();//初始化,产生随机数
private:
    int nums[N];
    void freearr(int*);//释放内存
    void swap(int&, int&);//交换
    void search_maxmin(int&, int&);//找最大值最小值
    int min(unsigned int a, unsigned int b) { return (a > b) ? b : a; }
    void merge_recursion(int *, unsigned int,unsigned int);//递归实现归并排序
    void shell_recursion(unsigned int);//递归实现希尔排序
    void insertionForShell(unsigned int,unsigned int);//辅助希尔排序的插入排序
    void quick_recursion(unsigned int, unsigned int);//快速排序实现
};

//错误代码:
//无论如何都无输出
//形参:区间开始位置、区间结束位置
void Solution::quick_recursion(unsigned int start, unsigned int end)
{
    if (start > end)  return;

    //遍历计数器
    int low = start,high = end;

    //基准数
    int pivot = nums[high];

    //移动标志,1 - 左,2 - 右
    int flag = 1;

    while(high > low)
    {
        if (flag == 1)
        {
            if (nums[low] > pivot)
            {
                swap(nums[high--], nums[low]);
                flag = 2;
                continue;
            }
            ++low;
            continue;
        }
        if (flag == 2)
        {
            if (nums[high] < pivot)
            {
                swap(nums[low++], nums[high]);
                flag = 1;
                continue;
            }
            --high;
            continue;
        }
    }
    swap(nums[low], nums[end]);

    //左右递归
    quick_recursion(start, low - 1);//左
    quick_recursion(low + 1, end);//右
}

void Solution::sortColors_quickmain()
{
    //快速排序
    quick_recursion(0, N - 1);
}

//错误代码:
//为什么报溢出错误?找不出错误……………………………………………………………………………………………………………………………
void Solution::sortColors_counting()
{
    //为节约空间,找最值
    int max, min;
    search_maxmin(max, min);

    //创建辅助数组
    int* nn = (int*)malloc(sizeof(int) * (max - min + 2));

    //初始化辅助数组
    int i;
    for (i = 0; i < (max - min + 1); ++i)  nn[i] = 0;

    //待排序数组统计
    for (i = 0; i < N; ++i)  ++nn[nums[i] - min];

    //辅助数组数据复制回原数组
    i = 0;
    for (int j = 0; j < (max - min + 1);++j)
    {
        for(int k = 0;k < nn[j];++k)  nums[i++] = j;
    }
        
    //释放空间
    freearr(nn);
}

void Solution::insertionForShell(unsigned int iseg,unsigned int start)
{
    int j;
    int insertion = nums[start];
    for (j = start; j > (iseg - 1); j -= iseg)
    {
        if (nums[j - iseg] < insertion)  break;
        nums[j] = nums[j - iseg];
    }
    nums[j] = insertion;
}

void Solution::sortColors_shellmain()
{
    //调用递归希尔函数
    shell_recursion(N / 2);
}

//形参:增量、开始位置
void Solution::shell_recursion(unsigned int iseg)
{
    //出口:增量为 0
    if (iseg <= 0)  return;

    //增量变小,调用递归
    shell_recursion(iseg / 2);
    
    //每个增量控制到不同区间,每个区间使用插入排序
    int i;
    for (i = iseg; i < N; ++i)  insertionForShell(iseg, i);
}

//形参:辅助数组、区间开始位置、区间末尾位置
void Solution::merge_recursion(int *tmp,unsigned int start,unsigned int end)
{
    if (start >= end)  return;//一个元素不用分
    int mid = start + (end -  start) / 2;//二分点
    //左右区间的首尾
    int start1 = start, end1 = mid;
    int start2 = mid + 1, end2 = end;
    merge_recursion(tmp, start1, end1);//左
    merge_recursion(tmp, start2, end2);//右
    int i = start;//辅助数组计数器
    //并入
    while ((start1 <= end1) && (start2 <= end2))
        tmp[i++] = (nums[start1] < nums[start2]) ? nums[start1++] : nums[start2++];
    while (start1 <= end1)  tmp[i++] = nums[start1++];
    while (start2 <= end2)  tmp[i++] = nums[start2++];
    while (start <= end)  nums[start] = tmp[start++];
}

//错误代码:
//最后一次比较不成功?报错“不能引用 NULL 指针”?……………………………………………………………………………………………………………………………
void Solution::sortColors_mergemain()
{
    int* nn = (int*)malloc(sizeof(int) * N);//辅助数组
    //merge_recursion(tmp, 0, N - 1);//递归实现
    //循环实现
    // 指针指向两个数组
    int* oo = nums;
    //增量,istep 表示每个区间各自左右区间的元素个数,如只有两个元素时,左区间和右区间都是一个元素
    int istep;
    //并入的数组计数器
    int ii;
    //每次区间的首位置
    int start;
    //二分数组的变量
    int low, mid, high;
    //右区间首位置
    int start1,start2;
    //待排序数组比较并入辅助数组
    for (istep = 1; istep < N; istep *= 2)
    {
        for (start = 0; start < N; start += istep * 2)
        {
            low = start;
            high = min(low + istep * 2, N);
            mid = min(low + istep, N);
            ii = start;
            start1 = low;
            start2 = mid;
            while (start1 < mid && start2 < high)  nn[ii++] = (oo[start1] > oo[start2]) ? oo[start2++] : oo[start1++];
            while (start1 < mid)  nn[ii++] = oo[start1++];
            while (start2 < high)  nn[ii++] = oo[start2++];
        }
        //交换指针指向
        int* tmp = oo;
        oo = nn;
        nn = tmp;
    }
    //最后指针指向最后并入的数组
    if (oo != nums)  memcpy(nums, nn, sizeof(int) * N);
}

void Solution::sortColors_insertion()
{
    int i, j;//i遍历,j后移数组
    for (i = 1; i < N; ++i)
    {
        int tmp = nums[i];
        for (j = i; (--j) && (nums[j - 1] > tmp); )
            nums[j] = nums[j - 1];
        nums[j] = tmp;
    }
}

void Solution::sortColors_bubble() 
{
    int n = sizeof(nums) / sizeof(nums[0]);
    /*int i, j;
    while (--n)//冒泡次数
    {//往后冒
        i = 0;
        j = i + 1;
        while (i < n)//比较直到上一次冒到最后的元素的前两位(因为j = i + 1)
        {
            if (nums[j] < nums[i])  swap(nums[j], nums[i]);
            ++i; ++j;
        }
    }*/
    //法二:
    for (int i = 0; i < n; ++i)//可以看成 i 是已经冒到后面的元素个数
        for (int j = 0; j < n - 1 - i;++j)//j 等于上一次冒到最后的元素的前两位,因为下面比较时使用的是 j + 1
            if (nums[j] > nums[j + 1])  swap(nums[j], nums[j + 1]);
}

void Solution::sortColors_selection()
{
    int n = sizeof(nums) / sizeof(nums[0]);
    int i, j, k;//i待排序,j查找,k标记,放到前面
    for (i = 0; i < n - 1; ++i)//最后一位自然有序,不需要多此一举
    {
        k = i;//k 标记为起始位置
        for (j = i + 1; j < n; ++j)//j 从 i 的下一位开始查找
            if (nums[j] < nums[k])  k = j;
        swap(nums[k], nums[i]);
    }
}

Solution::Solution()
{
    for (int i = 0; i < N; ++i)  nums[i] = rand() % 1000;//产生 0 - 1000 的随机数
}

void Solution::output()
{
    for (int i = 0; i < N; ++i)  cout << nums[i] << " ";
}

void Solution::swap(int &a,int &b)
{
    int tmp = a;
    a = b;
    b = tmp;
    return;
}

void Solution::search_maxmin(int& max, int& min)
{
    max = min = nums[0];
    int i = N;
    while (i--)
    {
        if (max < nums[i])  max = nums[i];
        if (min > nums[i])  min = nums[i];
    }
    return;
}

void Solution::freearr(int* arr) { free(arr); }

int main()
{
    Solution s1;
    //s1.sortColors_bubble();//测试冒泡排序
    //s1.sortColors_selection ();//测试选择排序
    //s1.sortColors_insertion();//测试插入排序
    //s1.sortColors_merge();//测试归并排序,递归归并成功,循环归并不成功
    //s1.sortColors_shellmain();//测试希尔排序
    //s1.sortColors_counting();//测试计数排序不成功
    //s1.sortColors_quickmain();//测试快速排序不成功
    s1.output();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值