跟着Datawhale重学数据结构与算法(3)---排序算法

开源链接:【 教程地址 】【电子网站
【写博客的目的是记录自己学习过程,方便自己复盘,专业课复习】

数组排序:

数组排序 冒泡排序 选择排序 插入排序 归并排序 希尔排序 快速排序 堆排序 计数排序 桶排序 基数排序

1.冒泡排序

1.1 算法思想
其实就跟线性代数里面的求逆序数的思想一样,ε=(1234),数当前数字与前面数字相比,有几个比它大的,然后加起来就是交换次数。
ε=(2143)=0+1+0+1=2,那么要成从小打到排列就要交换2次。
冒泡排序的本质也是两两交换,再用上面举例子,最坏的情况是ε=(4321)=0+1+2+3=6,我得交换6次。
step1:把最大的元素通过交换到正确的位置,我需要交换三次。
step2:把第二大元素通过俩俩交换到正确的位置,我需要交换俩次。
......
last step:最后剩的那个元素就是最小元素,无需排序,因此就完成了排序。
你可以推广到n个元素的排序。

可见,如果我们要用程序实现这个算法,就要使用双层循环,内层循环用来保证每次将一个元素放到对的位置,外层循环用来遍历整个数组。

1.2代码实现:
cpp 复制代码
/* 冒泡排序 */
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}
void bubbleSort(vector<int> &nums) {
    for (int i = nums.size() - 1; i > 0; i--) {
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                swap(nums[j], nums[j + 1]);
            }
        }
    }
}
1.3 优化代码

我们想这样一个问题,假如还是上面那个逆序数,如果这个数组他没有排好,那么他一定存在一个元素会发生交换操作,那反过来说,(逆否命题也成立),如果元素没有发生交换,那么说明这个数组元素是排列好了的。因此,我们就可以用一个bool变量放在内层交换函数后面,如果发生了交换,那么继续,如果没发生交换,那么直接退出for循环。

cpp 复制代码
/* 冒泡排序 */
void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}
void bubbleSortWithFlag(vector<int> &nums) {
    for (int i = nums.size() - 1; i > 0; i--) {
        bool flag = false; // 初始化标志位
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                swap(nums[j], nums[j + 1]);
                flag = true;
		}
        }
        if (!flag)
            break; // 此轮"冒泡"未交换任何元素,直接跳出
    }
}

要掌握的一些排序算法的复杂度:

2.选择排序

2.1 算法思想
就是分为两个状态空间,一个是未排序的状态空间,一个是已经排序好的状态空间,原理也非常简单,就是
每轮从未排序的区间选择最小的元素放在排序好的空间的末尾。,直到未排序好的状态空间仅剩一个元素肯定为数组的最大值。
2.2 代码实现
cpp 复制代码
void selectionSort(vector<int> &nums) {
    int n = nums.size();
    for (int i = 0; i < n - 1; i++) {
        int minIndex = i;
        for (int j = i + 1; j < n; j++) {
            if (nums[j] < nums[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex != i) {
            swap(nums[i], nums[minIndex]);
        }
    }
}

3.插入排序

3.1 算法思想
其实插入排序跟我们打扑克的时候整理扑克的时候很像,所以他的算法流程其实也很简单。
step1:先假设我们揭到的第一张牌为基准。
step2:从第二张牌开始,依次与基准进行比较将他插入到合适的位置,这样前两个元素就排好序了。
以此类推.......
依次将当前元素插入到已经排序好的子数组中。

插入排序的核心思想是不断地将当前元素插入到已经排序好的子数组中,以保持子数组的有序性。

3.2 代码实现:
cpp 复制代码
void insertionSort(vector<int> &nums) {
    int n = nums.size();
    for (int i = 1; i < n; i++) {
        int key = nums[i]; // 当前待插入的元素
        int j = i - 1;//排序好的子序列个数

        // 将当前元素插入到已经排序好的子数组中的合适位置
        // 从后往前进行比较,不符合条件就插入到此位置中。
        while (j >= 0 && nums[j] > key) {
            nums[j + 1] = nums[j]; // 将比当前元素大的元素往后移动一个位置
            j--;
        }
        nums[j + 1] = key; // 插入当前元素到合适的位置
    }
}

4.归并排序

4.1 算法思想
分割数组:将待排序数组递归地分割成长度大约相等的两个子数组,直到每个子数组只包含一个元素为止。
合并数组:将两个有序的子数组合并成一个有序的数组。合并过程中,逐个比较两个子数组的元素,并将较小的元素放入临时数组中,直到两个子数组都被合并完毕。
重复步骤 1 和步骤 2:对分割后的子数组进行递归排序,并合并已经排序好的子数组,直到所有子数组都合并成一个完整的有序数组。

归并排序的时间复杂度为 O(n log n),其中 n 是待排序数组的大小。它的优点是稳定性好、适用于大型数据集和链表等数据结构。

我感觉这个方法很amazing。用到了递归的思想,分而治之,再合并。跟深度学习中的深度可分离卷积(Depthwise Convolution)或者说shuffleNet很像,其实就是分别卷积,然后再使用1x1卷积共享参数,保证信息的交流。算法是互通的,对于归并排序来说,先拆分着进行排序,然后再合并起来进行排序。

4.2 代码实现
cpp 复制代码
// 合并两个有序数组
void merge(vector<int> &nums, int left, int mid, int right) {
    int n1 = mid - left + 1; // 左侧子数组的长度
    int n2 = right - mid; // 右侧子数组的长度
    // 创建临时数组用于存放合并后的结果
    vector<int> temp(n1 + n2);
    // 将左右两个子数组合并到临时数组中
    int i = left, j = mid + 1, k = 0;
    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j]) {
            temp[k++] = nums[i++];
        } else {
            temp[k++] = nums[j++];
        }
    }

    // 将剩余的元素复制到临时数组中
    while (i <= mid) {
        temp[k++] = nums[i++];
    }
    while (j <= right) {
        temp[k++] = nums[j++];
    }

    // 将临时数组的元素复制回原数组中
    for (int p = 0; p < k; p++) {
        nums[left + p] = temp[p];
    }
}

// 归并排序
void mergeSort(vector<int> &nums, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(nums, left, mid); // 对左侧子数组进行排序
        mergeSort(nums, mid + 1, right); // 对右侧子数组进行排序
        merge(nums, left, mid, right); // 合并两个有序子数组
    }
}

5.希尔排序

5.1 算法思想

希尔排序是一种改进的插入排序算法,也称为缩小增量排序。它的基本思想是将待排序数组按一定步长进行分组,对每组进行插入排序,然后逐步减小步长,重复上述步骤,直到步长为 1,此时数组基本有序,最后再进行一次插入排序。

希尔排序的关键在于选择合适的步长序列,也就是元素间隔数gap,不同的步长序列会影响排序的效率。通常使用的步长序列有希尔原始提出的序列(逐步除以 2)、Hibbard 序列、Sedgewick 序列等。

我感觉哈其实有种层次聚类的意思,通过逐步减小步长来逐渐减小逆序对,来使得数组变得有序,来减少元素的移动次数。我记得层次聚类有种方法就是从大到小每次选择一个阈值进行聚类,逐渐减小阈值,直到所有样本都得到合理的分类即可。

5.2 代码实现
cpp 复制代码
class Solution {
public:
    std::vector<int> shellSort(std::vector<int>& nums) {
        int size = nums.size();
        int gap = size / 2;

        while (gap > 0) {
            for (int i = gap; i < size; i++) {
                int temp = nums[i];
                int j = i;
                while (j >= gap && nums[j - gap] > temp) {
                    nums[j] = nums[j - gap];
                    j -= gap;
                }
                nums[j] = temp;
            }
            gap /= 2;
        }
        return nums;
    }

    std::vector<int> sortArray(std::vector<int>& nums) {
        return shellSort(nums);
    }
};

6.快速排序

6.1 算法思想
快速排序并不快,它使用分治策略来对一个数组进行排序。快速排序的基本思想是选择一个元素作为基准(pivot),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,(左边比基准都小,右边比基准都大),然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。(由大到小)。
6.2 代码实现:
cpp 复制代码
void quickSort(vector<int>& arr, int low, int high) {
    if (low < high) {
        // 选取基准元素为左端点
        int pivot = arr[low];
        int i = low;
        // 将小于基准的元素移动到分割点左侧,大于基准的元素移动到右侧
        for (int j = low + 1; j <= high; j++) {
            if (arr[j] < pivot) {
                i++;
                swap(arr[i], arr[j]);
            }
        }
        // 基准元素交换到中间
        swap(arr[low], arr[i]);
        // 递归排序分割点左侧和右侧的子数组
        quickSort(arr, low, i - 1);
        quickSort(arr, i + 1, high);
    }
}

7.堆排序

7.1 算法理解

堆得结构分为大根堆和小根堆,是一个完全二叉树(从形式上讲它是个缺失的的三角形,但所缺失的部分一定是右下角某个连续的部

分,最后那一行可能不是完整的)。大小根堆从字面意思上就可以理解为树从根节点向下排列的顺序。

采用顺序结构(数组)的形式来表示完全二,能够chong分利用存储空间。

7.2 代码实现
cpp 复制代码
//使用数组来存储大顶堆(左孩子和哟孩子都小于父节点)
//维护堆的性质:如果父节点不满足大顶堆的性质,那就将孩子的最大值与父节点进行交换
//这样就满足了大顶堆的性质,复杂度O(logn)
建堆:(变成大顶堆),复杂度O(n)

//SO:首先将数列变成大顶堆的形式,然后自下而上,从底部与根节点进行交换,交换后底部元素
//从树中删除,删除完后放在数列的最右端,更新此时的二叉树和数组,然后继续将第二个元素与根节点
//元素进行交换,继续....直到树只剩一个元素,操作完成。
void heapify(int arr[], int n, int i){
/*数组,数组长度,待维护节点下标*/
int largest = i;
int lson = i*2 +1;
int rson = i*2 +2;

if(lson < n && arr[largest] < arr[lson])
	largest = lson;
if(rson < n && arr[largest] < arr[rson])
	largest = rson;
if (largest !=i)
{
	swap(&arr[largest],&arr[i]);
	heapify(arr,n,largest);
}
}
//堆排序入口
void heap_sort(int arr[], int n){
	int i;
	//建堆
	for(i=n/2-1;i>=0;i--)
		heapify(arr,n,i);
	//排序
	for(i=n-1;i>0;i--)
	{
		swap(&arr[i],&arr[0]);
		heapify(arr,i,0);
	}
}

稳定性:不稳定

8. 计数排序

8.1算法原理

通过计数而不是比较来进行排序,适用于范围比较小的整数序列

原始数组->计数数组->累计数组->最终结果

8.2 代码实现:
cpp 复制代码
void counting_sort(int arr[], int len)
{
    if (len < 1) return;

    // 寻找最大的元素
    int max = arr[0];
    for (size_t i = 1; i < len; i++)
        if (arr[i] > max) max = arr[i];

    // 分配一个长度为max+1的数组存储计数,并初始化为0
    int *count = (int *)malloc(sizeof(int) * (max + 1));
    memset(count, 0, sizeof(int) * (max + 1));

    // 计数
    for (size_t i = 0; i < len; i++)
        count[arr[i]]++;

    // 统计计数的累计值
    for (size_t i = 1; i < max + 1; i++)
        count[i] += count[i - 1];

    // 创建一个临时数组保存结果
    int *output = (int *)malloc(sizeof(int) * len);

    // 将元素放到正确的位置上
    for (size_t i = 0; i < len; i++)
    {
        output[count[arr[i]] - 1] = arr[i];
        count[arr[i]]--;
    }

    // 将结果复制回原数组
    for (size_t i = 0; i < len; i++)
        arr[i] = output[i];
}

9.桶排序

9.1 算法原理

将待排序的元素分到有限数量的桶中,每个桶再分别进行排序,最后将各个桶中的元素合并得到排序结果。桶排序的核心思想是将一组数据分割成一定数量的桶,然后对每个桶中的数据进行排序,最后将所有桶中的数据按照顺序合并起来。首先,初始化一个足够数量的空桶。然后遍历待排序的数组,将每个元素放入相应的桶中;对每个非空的桶中的元素进行排序(可以使用其他排序算法,如插入排序);按照桶的顺序将所有非空桶中的元素合并得到排序结果。

9.2 代码实现
cpp 复制代码
// 桶排序函数
void bucketSort(vector<float>& arr) {
    int n = arr.size();
    // 创建足够数量的空桶
    vector<vector<float>> buckets(n);

    // 将每个元素放入相应的桶中
    for (int i = 0; i < n; ++i) {
        int bucket_index = n * arr[i];
        buckets[bucket_index].push_back(arr[i]);
    }

    // 对每个非空桶中的元素进行排序(这里使用插入排序)
    for (int i = 0; i < n; ++i) {
        sort(buckets[i].begin(), buckets[i].end());
    }

    // 合并所有非空桶中的元素得到排序结果
    int index = 0;
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < buckets[i].size(); ++j) {
            arr[index++] = buckets[i][j];
        }
    }
}

10.基数排序

10.1 算法思想

基数排序是一种非比较性的排序算法,它按照数字的位数来进行排序。基数排序可以分为 LSD最低位优先(Least Significant Digit)和 MSD最高位优先(Most Significant Digit)两种实现方式。下面我们来介绍 LSD 基数排序的算法思想和实现。

LSD 基数排序的基本思想是从数字的最低位(个位)开始,依次对数字进行排序,直到最高位(最高位数的位数)。在每一轮排序过程中,对所有数字根据当前位上的数字进行桶排序。通过这样的逐位排序过程,最终可以得到排好序的结果。

这个方法跟人类很像,我们通常来比较两个事物所有的特征(差异)来区分,LSD这个就相当于先从个位进行排序,个位排完后,在向上观察最大的特征差异,然后到第一位的比较。

10.2 代码实现
cpp 复制代码
//代码也非常简单
void radixSort(vector<int>& arr) {
    // 获取数组中的最大值
    int max_num = *max_element(arr.begin(), arr.end());

    // LSD 基数排序
    for (int digit = 1; max_num / digit > 0; digit *= 10) {
        // 创建 10 个桶,用于存放当前位上的数字
        vector<vector<int>> buckets(10);

        // 将数组中的元素按照当前位上的数字放入相应的桶中
        for (int num : arr) {
            int digit_value = getDigit(num, digit);
            buckets[digit_value].push_back(num);
        }

        // 从桶中按顺序取出元素,替换原数组中的元素
        int index = 0;
        for (auto& bucket : buckets) {
        // auto 是为了让编译器自动推导出 bucket 的类型
            for (int num : bucket) {
                arr[index++] = num;
            }
            bucket.clear();
        }
    }
}

Leetcode之旅~:

【1】LCR.164破解闯关密码

直接偷懒,写到这累的不行了,我直接sort(快速排序)+拼接字符串。

cpp 复制代码
class Solution {
public:
    // 自定义比较函数,用于确定两个数字拼接后的结果的大小
    static bool compare(const int& x, const int& y) {
        string xy = to_string(x) + to_string(y);
        string yx = to_string(y) + to_string(x);
        return xy < yx;
    }

    string crackPassword(vector<int>& password) {
        // 将数组按照自定义的比较函数排序
        sort(password.begin(), password.end(), compare);
        
        // 将排序后的数组拼接成字符串
        string result = "";
        for (int num : password) {
            result += to_string(num);
        }
        return result;
    }
};

【2】移动0

直接双指针遍历,移动非零元素,然后遍历完后,直接left后面全部赋0.

cpp 复制代码
class Solution {
public:
    void moveZeroes(vector<int>& nums) {
        int left = 0; // left 指针
        int n = nums.size();
        
        // 遍历数组
        for (int right = 0; right < n; right++) {
            if (nums[right] != 0) {
                // 将非零元素放到 left 指针位置
                nums[left++] = nums[right];
            }
        }
        
        // 将 left 指针后面的所有元素置为 0
        while (left < n) {
            nums[left++] = 0;
        }
    }
};

【3】[数组排序](https://leetcode.cn/problems/sort-an-array/)

直接用前面冒泡排序的代码-》超出时间限制emmmm

快速排序也超出时间限制了,这怎么搞,我想想,数组太大了,那就小规模用插入排序更快一些,大数组用快速。

cpp 复制代码
class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        quickSort(nums, 0, nums.size() - 1);
        return nums;
    }
    
    // 快速排序函数
    void quickSort(vector<int>& nums, int low, int high) {
        if (low < high) {
            // 对小规模数组(<=10)使用插入排序
            if (high - low + 1 <= 10) {
                insertionSort(nums, low, high);
                return;
            }
            
            // 随机选择基准元素
            int pivotIndex = rand() % (high - low + 1) + low;
            swap(nums[pivotIndex], nums[high]);
            pivotIndex = partition(nums, low, high);
            quickSort(nums, low, pivotIndex - 1);
            quickSort(nums, pivotIndex + 1, high);
        }
    }
    
    // 划分函数,返回分割点的位置
    int partition(vector<int>& nums, int low, int high) {
        int pivot = nums[high]; // 基准元素
        int i = low - 1; // 定义分割点的位置

        for (int j = low; j < high; j++) {
            if (nums[j] < pivot) {
                i++;
                swap(nums[i], nums[j]);
            }
        }
        
        swap(nums[i + 1], nums[high]); // 将基准元素放到正确的位置
        return i + 1;
    }
    
    // 插入排序函数
    void insertionSort(vector<int>& nums, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int key = nums[i];
            int j = i - 1;
            while (j >= low && nums[j] > key) {
                nums[j + 1] = nums[j];
                j--;
            }
            nums[j + 1] = key;
        }
    }
};

通过了,累了,不敲了

参考资料:

[1] 【教程地址 】[电子网站]

[2] Hello 算法教程

[3] https://blog.csdn.net/qq_39181839/article/details/109478094

[4] https://blog.csdn.net/u010452388/article/details/81283998

感谢:DataWhale社区

相关推荐
刚学HTML12 分钟前
leetcode 05 回文字符串
算法·leetcode
Yan.love26 分钟前
开发场景中Java 集合的最佳选择
java·数据结构·链表
AC使者31 分钟前
#B1630. 数字走向4
算法
冠位观测者35 分钟前
【Leetcode 每日一题】2545. 根据第 K 场考试的分数排序
数据结构·算法·leetcode
古希腊掌管学习的神1 小时前
[搜广推]王树森推荐系统笔记——曝光过滤 & Bloom Filter
算法·推荐算法
qystca1 小时前
洛谷 P1706 全排列问题 C语言
算法
古希腊掌管学习的神1 小时前
[LeetCode-Python版]相向双指针——611. 有效三角形的个数
开发语言·python·leetcode
浊酒南街1 小时前
决策树(理论知识1)
算法·决策树·机器学习
就爱学编程2 小时前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
学术头条2 小时前
清华、智谱团队:探索 RLHF 的 scaling laws
人工智能·深度学习·算法·机器学习·语言模型·计算语言学