排序

冒泡排序(bubble sort)

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

冒泡排序还可以优化,比如本来要外循环6次,但是在第4次外循环的时候已经发现没有可以交换的了,那么就可以提前结束排序过程了

void bubbleSort(std::vector<int>& nums)
{
	int n = nums.size();
	if (n <= 1) return;

	bool flag = false;//提前退出冒泡循环的标志
	for (int i = 0; i < n; ++i) {
		flag = false;
		for (int j = 0; j < n - i - 1; ++j) {
			//正常,跳过
			if (nums[j] <= nums[j + 1]) continue;
			//不正常
			std::swap(nums[j], nums[j + 1]);
			flag = true;
		}
		if (!flag) break;
	}
}

插入排序(insertion sort)

我们先来看一个问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。

插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个数据 a 插入到已排序区间时,需要拿 a 与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,我们还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素 a 插入。

void insertionSort(std::vector<int>& nums)
{
	int n = nums.size();
	if (n <= 1) return;

	for (int i = 1; i < n; ++i) {
		int tmp = nums[i];
		int j = i - 1;
		//从i往前查找
		for (; j >= 0; --j)
		{
			if (nums[j] > tmp)
			{
				nums[j + 1] = nums[j];//如果比要插入的tmp值大,就要移动到tmp后面
			}
			else {
				break;
			}
		}
		nums[j+1] = tmp;//此时的nums[j+1]不比tmp大,可以插入在此了. 为什么是j+1, 因为跳出for循环的时候j-1了
	}
} 

选择排序(selection sort)

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

void selectionSort(std::vector<int>& nums) 
{
	int n = nums.size();
	if (n <= 1) return;

	for (int i = 0; i < n; ++i) 
	{
		int minNum = nums[i];
		int minNumIndex = -1;
		for (int j = i + 1; j < n; ++j)
		{
			if (nums[j] >= minNum) continue;
			minNum = nums[j];
			minNumIndex = j;
		}
		if (minNumIndex >= 0)
		{
			std::swap(nums[i], nums[minNumIndex]);
		}
	}
}

归并排序(merge sort)

归并排序的核心思想还是蛮简单的。如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

因此核心思想就是:先拆 -> 不能再拆 -> 再合 -> 不能再合,这样就对应到

void mergeSort_c(std::vector<int>& nums, int left, int right)
{
	if (left >= right) return;

	int mid = left + (right - left) / 2;
	//拆
	mergeSort_c(nums, left, mid);
	mergeSort_c(nums, mid+1, right);
	//合
	std::vector<int> tmp;
	int i = left, j = mid + 1;
	while (i <= mid && j <= right)
	{
		if (nums[i] < nums[j]) {
			tmp.push_back(nums[i]);
			i++;
		}
		else {
			tmp.push_back(nums[j]);
			j++;
		}
	}
	while (i <= mid) {
		tmp.push_back(nums[i]);
		i++;
	}
	while (j <= right) {
		tmp.push_back(nums[j]);
		j++;
	}
	//tmp替换掉nums里的[left,right]区间
	for (int i = 0; i < tmp.size(); ++i)
	{
		nums[left + i] = tmp[i];
	}
}
void mergeSort(std::vector<int>& nums)
{
	int n = nums.size();
	if (n <= 1) return;
	mergeSort_c(nums, 0, n - 1);
}

快速排序(quick sort)

快排的思想是这样的:如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。

void quickSort_c(std::vector<int>& nums, int left, int right)
{
	if (left >= right) return;
	int i = left;
	int j = right;
	int base = nums[left];
	while (i < j) {
		//从右向左,如果遇到了小于base的停下
		//注意:一定要先从右向左扫描,因为上一次交换后nums[j]>base,nums[i]<base,
		//此时待扫描区间[i,j],若先从左往右扫描,当[i,j-1]都小于base时,会在j的位置跳出while,
		//此时i==j,nums[j]>base, nums[left]不能和nums[j]交换,否则不满足左边都是小于base
		//同理,如果base = nums[right],则一定要先从左往右扫描,保证退出while时右边都大于base
		while (nums[j] >= base && i < j) {
			j--;
		}
		//从左向右,如果遇到了大于base的停下
		while (nums[i] <= base && i < j) {
			i++;
		}
		if (i < j) {
			//此时nums[i]>base, nums[j]<base,因此对调即可实现 小于base的都在左边,大于base的都在右边
			std::swap(nums[i], nums[j]);
		}
		/*
		for (auto& i : nums) {
			std::cout << i << ",";
		}
		std::cout << "    " << i << "," << j << "\n";
		*/
	}
	//此时i=j, 将base(nums[left]) 和 nums[i] 对调
	nums[left] = nums[i];
	nums[i] = base;
	//递归处理 左半部分, 右半部分
	quickSort_c(nums, left, i - 1);
	quickSort_c(nums, i + 1, right);

}
void quickSort(std::vector<int>& nums)
{
	int n = nums.size();
	if (n <= 1) return;
	quickSort_c(nums, 0, n-1);
}

这里需要注意:一定要先从右向左扫描,因为上一次交换后nums[j]>base,nums[i]<base,此时待扫描区间[i,j],若先从左往右扫描,当[i,j-1]都小于base时,会在j的位置跳出while,此时i==j,nums[j]>base, nums[left]不能和nums[j]交换,否则不满足左边都是小于base。同理,如果base = nums[right],则一定要先从左往右扫描,保证退出while时右边都大于base。

例如:
nums = {4,5,6,3,2,1,23,18,56,88,19,32,10}, base = 4
当区间[i,j]为[2,4]时,此时对应值是{2,3,6}, 应该在nums[i]==3的位置上跳出循环,而不是nums[i]==6

基数排序(radix sort)

核心思想是把每个数按每一位分桶

以 11 10 19 28 20 39 32 9 5 8 0 为例。

  • 第一轮 : 按个位数排序
0 10,20, 0
1 11
2 32
3
4
5 5
6
7
8 28, 8
9 19, 39, 9

此时对应排序为 : 10,20, 0, 11, 32, 5, 28, 8, 19, 39, 9

  • 第二轮 : 按十位数排序
0 0,5,8,9
1 10,11,19
2 20, 28
3 32,39
4
5
6
7
8
9

此时对应排序为 : 0,5,8,9,10,11,19,20,28,32,39

因为最多只有两位,此时已完成了排序。如果有更多位,则接着进行。

// 这里为了简便,以字符串表示数字,并且假定 无负数 且 每个数字长度相同
void radixSort(vector<string>& nums) {
    int n = nums.size();
    if (n == 0) return;
    int m = nums[0].size();
    vector<int> lenVec(n);
    for (int i = 0; i < n; ++i) {
        lenVec[i] = i;
    }
    for (int len = 0; len < m; ++len) {
        vector<vector<int>> radix(10);
        for (int index : lenVec) {
            radix[nums[index][m - 1 - len] - '0'].emplace_back(index);
        }
        int index = 0;
        for (const auto& i : radix) {
            for (auto j : i) {
                lenVec[index++] = j;
            }
        }
    }
    auto copy = nums;
    for (int i = 0; i < n; ++i) {
        nums[i] = copy[lenVec[i]];
    }
}

参考资料

极客时间 数据结构与算法之美 王铮

posted @ 2020-10-28 22:04  miyanyan  阅读(146)  评论(0编辑  收藏  举报