【数据结构与算法C++实现】3、排序算法

原视频为左程云的B站教学



以下所有的swap()函数,函数定义为

void swap(int& a, int& b)
{
    
    
	int t = a;
	a = b;
	b = t;
}
// 也可以用异或,但不能传入同一个变量,可以是不同变量相同值
void swap(int& a, int& b)
{
    
    
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
}

1 冒泡排序 O ( N 2 ) O(N^2) O(N2)(与数据状况无关)

最基础、最简单的排序,没啥好说的
请添加图片描述

#incldue <vector>
void bubbleSort(std::vector<int>& arr) 
{
    
    
    for (int i = 0; i < arr.size() - 1; i++) {
    
    
        for (int j = 0; j < arr.size() - 1 - i; j++) {
    
    
            if (arr[j] > arr[j+1]) {
    
            // 相邻元素两两对比
				swap(arr[j], arr[j+1]);
            }
        }
    }
}

2 选择排序 O ( N 2 ) O(N^2) O(N2)(与数据状况无关)

基本思想是每次从待排序的元素中选择最小(或最大)的元素,放到已排序序列的末尾。选择排序的主要步骤如下:

  • 1.遍历待排序序列,设定当前位置为最小值的位置。
  • 2.从当前位置开始,依次比较当前元素与后面的元素,找到最小的元素,记录其位置。
  • 3.将最小元素与当前位置的元素进行交换。
  • 4.重复步骤2和步骤3,直到遍历完整个序列。
    请添加图片描述
#include<vector>
void selectionSort(std::vector<int>& arr)
{
    
    
	int len = arr.size();
	if (arr.empty() || len < 2) return;

	for (int i = 0; i < len -1; i++){
    
     // len -1 位置不用再循环了,因为最后剩一个必然是最值
		int minIndex = i;		// 在i ~ len-1 上寻找最小值的下标
		for (int j = i; j < len - 1; j++){
    
    
			minIndex = arr[j] < arr[minIndex] ? j : minIndex;
		}
		swap(arr[minIndex], arr[i]);
	}
}

3 插入排序 O ( N 2 ) O(N^2) O(N2)(与数据状况相关)

原理类似于对扑克牌手牌进行排序的过程。将未排序的元素逐个插入到已排序部分的合适位置。

逻辑顺序为先做到0~0有序, 然后0~1有序, 0~2 … 0~n-1有序

算法流程为:

  • 1.从第一个元素开始,该元素可以认为已经被排序。
  • 2.从未排序部分取第一个值,插入到已排序部分的适当位置。这个位置的选取方式为:把当前未排序值与左邻值对比,如果更小则交换位置。直到遇到边界或不比左邻值小则结束。(内循环:找适当位置插入)
  • 3.重复步骤2,直到所有元素都被插入到已排序序列中。(外循环:遍历每个未排序的数)

这个不像冒泡排序和选择排序是固定操作(数据状况无关)。插入排序中,如果给的就是有序的,那就外层循环每次比一下就完成了所以会是O(N), 但我们说时间复杂度都是 worst case 所以还是 O ( N 2 ) O(N^2) O(N2)

请添加图片描述

#include <vector>
void insertionSort(std::vector<int>& arr)
{
    
    
	int len = arr.size();
	if (arr.empty() || len < 2) return;
	// 0~0已经有序了
	for (int i = 1; i < len; i++){
    
     	// 0~i做到有序
		// 把当前值(j+1)对比左邻值(j),比他小则交换,不满足循环条件则说明找到合适位置了
		for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--){
    
     
			swap(arr[j+1], arr[j]);
		}
	}
}

在内层循环中,我们将当前元素 arr[j + 1] 与其左邻元素 arr[j] 进行比较,更小则换位,直到找到合适的插入位置。
循环结束(找到合适的位置)条件为:达到左边界 or 当前值比左邻值大

继续往下看建议先掌握 二分查找和基础递归

4 归并排序 O ( N l o g N ) O(NlogN) O(NlogN)(数据状况无关)

它的核心思想是将待排序的序列不断划分成更小的子序列,直到每个子序列只有一个元素,然后再将这些子序列两两合并,直到最终整个序列有序。

下面是归并排序的一般步骤:

  • 分割:将待排序的序列从中间位置分割成两个子序列,不断递归地将每个子序列继续分割,直到每个子序列只剩下一个元素。
  • 合并:将两个有序的子序列合并成一个有序序列。从两个子序列的第一个元素开始比较,将较小的元素放入临时数组中,并将对应子序列的索引向后移动一位,直到其中一个子序列的元素全部放入临时数组中。然后将另一个子序列的剩余元素直接放入临时数组中。
  • 重复合并:重复进行合并操作,直到所有子序列都合并为一个有序序列。

始终都是 O(NlogN) 的时间复杂度,与数据无关。虽然相对前面的冒泡、选择、插入排序更快,但是空间复杂度为O(N)

下图对于这个过程的描述是非常准确的,请添加图片描述

#include <vector>
// 第二阶段:merge阶段时,L~M 和 M~R 之间的数必然有序
void merge(std::vector<int>& arr, int L, int M, int R)
{
    
    
	std::vector help(R - L + 1);
	int i = 0;		// 临时数组的起始索引
	int p1 = L;		// 左半部分的起始索引
	int p2 = M + 1;	// 右半部分的起始索引

	//外排序,把两个子数组中的元素从小到大放到help数组
	while (p1 <= M && p2 <= R){
    
     
		help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
	}

 	// 如果左边部分还有剩的,全部依次加到help的末尾
	while (p1 <= M){
    
    
		help[i++] = arr[p1++];
	}
	// 同理如果右边还有剩的
	while (p2 <= R){
    
     
		help[i++] = arr[p2++];
	}

	// 结束,把临时数组的内容覆盖到原数组中
	for (i = 0; i < R - L + 1; i++){
    
    
		arr[L + i] = help[i]; // 注意arr从L位置开始写入的
	}
}

// 务必先看这个函数 这算是第一阶段拆分
void mergeSort(std::vector<int>& arr, int L, int R)
{
    
    
	if (L == R) return;	// 拆分完毕,不能再拆了
	
	int mid = L + ((R - L) >> 1);
	mergeSort(arr, L, mid);
	mergeSort(arr, mid+1, R);
	merge(arr, L, mid, R); // 执行到这一步的条件是 L == mid 且 mid+1 == R 即左右子树都已二分到只有一个元素
}

时间复杂度(参考2.1 master公式

  • T ( N ) = 2 ⋅ T ( N 2 ) + O ( N ) T(N) = 2·T(\frac{N}{2})+O(N) T(N)=2T(2N)+O(N)
  • a = 2; b = 2; d = 1
  • l o g a b = 1 = d log_ab=1=d logab=1=d,所以时间复杂度为 O ( N ⋅ l o g N ) O(N·logN) O(NlogN)

空间复杂度: O ( N ) O(N) O(N) 。因为每次merge的时候开辟一块空间,大小为N

4.1 归并排序的扩展(求数组小和)

题目:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

例子:[1,3,4,2,5]
1左边比1小的数,无;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数,1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16
要求时间复杂度O(NlogN),空间复杂度O(N)

如果每个位置的元素都遍历一遍左边所有元素,找出比它小的数,求和,很简单就能得到结果,时间复杂度为 O ( N 2 ) O(N^2) O(N2)

转变思维:不是找左边有多少比当前位置的数更小的数,然后求和,而是统计有多少个比当前位置的数更大的数,当前数就该累加多少次,而这个过程在归并排序中的 外排序中就可以做到

具体讲解视频精准空降,比看文字可好多了

#include <iostream>
#include <vector>

long long mergeAndCount(std::vector<int>& arr, int left, int mid, int right) 
{
    
    
    std::vector<int> temp(right - left + 1); // 临时数组用于存储合并后的结果
    int i = 0; // 临时数组的起始索引
    int p1 = left; // 左半部分的起始索引
    int p2 = mid + 1; // 右半部分的起始索引
    long long count = 0; // 记录小和的累加和

    while (p1 <= mid && p2 <= right) {
    
    
        if (arr[p1] <= arr[p2]) {
    
    
            // 重点:如果arr[p1]比arr[p2]小,则arr[p1]比p2后面所有数都小!
            count += arr[p1] * (right - p2 + 1);
            temp[i++] = arr[p1++];
        } else {
    
    
            temp[i++] = arr[p2++];
        }
    }

    while (p1 <= mid) temp[i++] = arr[p1++];
    while (p2 <= right) temp[i++] = arr[p2++];

    // 将临时数组的结果复制回原数组
    for (i = 0; i < R - L + 1; i++) {
    
    
        arr[left + i] = temp[i];
    }

    return count;
}

long long mergeSortAndCount(std::vector<int>& arr, int left, int right) 
{
    
    
    if (left >= right) {
    
    
        return 0; // 单个元素无小和
    }

    int mid = left + (right - left) / 2;
    long long leftCount = mergeSortAndCount(arr, left, mid); // 左半部分的小和
    long long rightCount = mergeSortAndCount(arr, mid + 1, right); // 右半部分的小和
    long long mergeCount = mergeAndCount(arr, left, mid, right); // 合并过程中的小和

    return leftCount + rightCount + mergeCount; // 总的小和
}

long long calculateSmallSum(std::vector<int>& arr) 
{
    
    
    if (arr.empty()) return 0; // 空数组无小和

    int left = 0;
    int right = arr.size() - 1;
    return mergeSortAndCount(arr, left, right);
}

int main() 
{
    
    
    std::vector<int> arr = {
    
    3, 1, 4, 2, 5};
    long long smallSum = calculateSmallSum(arr);
    std::cout << "Small Sum: " << smallSum << std::endl;
	
	std::cin.get();
    return 0;
}

5 快速排序

快排 3.0 (1.0 和 2.0 参考别处)

猜你喜欢

转载自blog.csdn.net/Motarookie/article/details/131383596