排序算法笔记

开始前准备一些工具函数

#pragma once

#include<iostream>
#include<vector>

using namespace std;

// 交换函数
void MySwap(int& num1, int& num2)
{
	int tmp = num1;
	num1 = num2;
	num2 = tmp;
}

// 打印函数
void PrintArray(vector<int>& nums)
{
	for (int i = 0; i < nums.size(); i++)
	{
		cout << nums[i] << ",";
	}
	cout << endl;
}


int main(void)
{
	vector<int> nums;
	srand((unsigned int)time(NULL));
	for (int i = 0; i < 10; i++)
	{
		nums.push_back(rand() % 10) ;
	}
	cout<< "排序前:\n"<<endl;

	PrintArray(nums);

	// 	BubbleSort(nums);
	// 	SelectSort1(nums);
	// 	SelectSort2(nums);
	// 	InsertSort(nums);
	//	ShellSort(nums);
	//	MergeSort(nums, 0, nums.size() - 1);
	//  QuickSort(nums, 0, nums.size() - 1);
	//  HeapSort(nums);

	printf("排序后:\n");
	PrintArray(nums);
	return 0;
}

1. 冒泡排序

思路:

两两比较,每次确定一个最高位

i:0~nums.size() - 1,记录数组nums进行nums.size()趟排序,每次确定index = nums.size() - i -1位置的数据(该趟遍历的最大值)

j:相邻两数比较的下标j和j+1,j从0开始,j+1最大到这趟遍历该确定数据的index,j + 1 = index

在这里插入图片描述

优化:

如果一趟比较中没有出现交换,说明数组已经有序,可以用bool DoSwap记录单趟的比较情况

代码:

// 冒泡排序: 
void BubbleSort(vector<int>& nums)
{
	// i记录比较的趟数,每一趟确定index = nums.size() - i - 1位置的数据,需要nums.size()趟
	for (size_t i = 0; i < nums.size(); i++)
	{
		// 记录单趟是否发生交换
		bool DoSwap = false;

		// j和j+1是比较数的下标,j+1最大为index = nums.size() - i - 1
		for (size_t j = 0; j + i + 1 < nums.size(); j++)
		{
			if (nums[j] > nums[j + 1])
			{
				MySwap(nums[j], nums[j + 1]);
				DoSwap = true;
			}
		}

		// 如果单趟不产生交换,说明数组已经有序
		if (!DoSwap) break;
	}
}

时间复杂度:平均 O( N^2)  最差 O( N^2)

额外空间:O(1)

2. 选择排序

思路:

nums.size()趟扫描,每趟记住最小值的下标,结束扫描后交换到对应的位置

i:0~nums.size() - 1,记录数组nums进行nums.size()趟排序,每次确定i 位置的数据(该趟遍历的最小值下标)

index:记录单趟扫描的最小值下标

j:i~nums.size() - 1,每一趟扫描从i位置开始直到数组尾

选择排序

代码:单趟选最小值

// 选择排序:选最小
void SelectSort1(vector<int>& nums)
{
	// i记录排序趟数,第i趟排序确定i处的数据(单趟排序最小值下标)
	for (size_t i = 0; i < nums.size(); i++)
	{
		// index记录当前趟的最小值下标
		int index = i;

		// j是单趟排序遍历i+1 到 nums.size() -1
		for (size_t j = i + 1; j < nums.size(); j++)
		{
			index = nums[index] < nums[j] ? index : j;
		}

		// 交换位置i的数据和单趟排序最小值
		MySwap(nums[i], nums[index]);
	

代码:单趟选最大值

// 选择排序:选最大
void SelectSort2(vector<int>& nums)
{
	// i记录排序趟数,第i趟排序确定nums.size() - i - 1处的数据(单趟排序最大值下标)
	for (size_t i = 0; i < nums.size(); i++)
	{
		// index记录当前趟的最大值下标
		int index = nums.size() - i - 1;

		// j是单趟排序遍历0 到 nums.size() - i - 1
		for (size_t j = 0; j < nums.size() - i - 1; j++)
		{
			index = nums[index] > nums[j] ? index : j;
		}

		// 交换位置nums.size() - i - 1的数据和单趟排序最大值
		MySwap(nums[nums.size() - i - 1], nums[index]);
	}
}

时间复杂度:平均 O( N^2)  最差 O( N^2)

额外空间:O(1)

3. 插入排序

思路:

i:0~nums.size() - 1,记录数组第i趟排序,先记录i处的数据cur_data找插入位置insert_index

j:i - 1 ~ 0 和i处数据比较的前序数据,如果j处数据比i处数据大,数据后移,j继续往前找;如果j处数据比处数据小,j + 1 处就是要找的位置

在这里插入图片描述

// 插入排序
void InsertSort(vector<int>& nums)
{
	// i记录排序趟数,第i趟排序抽出i处数据往前找插入位置
	for (int i = 1; i < nums.size(); i++)
	{
		// cur_data记录当前要插入元素的值, inset_index记录要插入的位置
		int cur_data = nums[i];
		int insert_index = i;
		// j是单趟搜索的下标,从i - 1 搜索到0
		for(int j = i - 1; j >= 0; j--)
		{
			// j处元素比当前值大,j处元素后移,j指针继续往前找
			if (nums[j] > cur_data)
			{
				nums[j + 1] = nums[j];
				insert_index = j;
			}
			else // j处元素小于等于当前值,j+1处是要插入的位置
			{
				insert_index = j + 1;
				break;
			}
		}

		// 将cur_data填入insert_index
		nums[insert_index] = cur_data;
	}
}

4. 希尔排序

思路:

优化的插入排序,插入排序每次往前找1步,希尔排序每次往前找步长

希尔排序步长可以开始选择长度的1/2,然后逐步减半直到1

在这里插入图片描述

// 希尔排序
void ShellSort(vector<int>& nums)
{
	// 定义一个步长
	int len = nums.size() / 2;

	while (len >= 1)
	{
		// 抽出i处数据往前找插入位置, i从1到nums.size() - 1
		for (int i = 1; i < nums.size(); i++)
		{
			// cur_data记录当前要插入元素的值, inset_index记录要插入的位置
			int cur_data = nums[i];
			int insert_index = i;

			// j是单趟搜索的下标,每次往前按步长找,直到0
			for (int j = i - len; j >= 0; j -= len)
			{
				// j处元素比当前值大,j处元素后移步长len,j指针继续往前找
				if (nums[j] > cur_data)
				{
					nums[j + len] = nums[j];
					insert_index = j;
				}
				else // j处元素小于等于当前值,j + len处是要插入的位置
				{
					insert_index = j + len;
					break;
				}
			}

			// 将cur_data填入insert_index
			nums[insert_index] = cur_data;
		}

		// 每趟排序完len减半
		len = len / 2;
	}
}

时间复杂度:平均 O( NlogN) 

额外空间:O(1)

5. 归并排序

思路:

局限的归并排序:有序的两个有序序列归并成一个序列

不局限的归并排序:递归到最小相邻的两个数就是有序的序列,进行两两合并

// 局限的归并排序:
// nums的A序列[left_A, right_A]有序,B序列[left_B, right_B]有序,左闭右闭原则
// 将A和B按顺序合并得到有序数列res
// 将res按下标对应替换到nums中
void SpecialMergeSort(vector<int>& nums, int left_A, int right_A, int left_B, int right_B)
{
	if (left_A < 0 || right_A >= nums.size()) return;
	if (left_B < 0 || right_B >= nums.size()) return;

	vector<int> res;

	// 双指针i指向A序列,j指向B序列
	int i = left_A;
	int j = left_B;

	// 比较移动ij指针,res按从小到大存储合并后的数据,直到其中一个序列走完
	while (i <= right_A && j <= right_B)
	{
		if (nums[i] < nums[j])
		{
			res.push_back(nums[i]);
			i++;
		}
		else
		{
			res.push_back(nums[j]);
			j++;
		}
	}

	// 把没走完的序列中的元素全部存到res中
	while(i <= right_A)
	{
		res.push_back(nums[i]);
		i++;
	}

	while (j <= right_B)
	{
		res.push_back(nums[j]);
		j++;
	}

	// 注意这里不能直接nums = res,归并排序分割的最小单元是两个数据,nums和res的size不一样大
	// 归并排序的两个子序列A和B是挨着的,所以res替换nums的[left_A, right_B]段的数据就行
	// nums = res;
	if (res.size() != right_B - left_A + 1) return;
	for (int i = 0; i <res.size(); i++)
	{
		nums[left_A + i] = res[i];
	}
}

// 通用的归并排序
// 递归思想:left到mid排序, mid到right排序 (左闭右闭原则)
// 将有序的两个数组用SpecialMergeSort合并
void MergeSort(vector<int>& nums, int left, int right)
{
	if (nums.empty() || left < 0 || right + 1 > nums.size()) return;
	if (left >= right) return;

	int mid = (left + right) / 2;

	// 左边数组排成有序[left, mid],左闭右闭
	MergeSort(nums, left, mid);
	// 右边边数组排成有序[mid + 1, right],左闭右闭
	MergeSort(nums, mid + 1, right);
	// 将左右两个有序数列合并
	SpecialMergeSort(nums, left, mid, mid + 1, right);

	/* 注意:这种分割方式会栈溢出,因为它在数组只有很少元素(如两个)时
    仍然进行不必要的递归调用。这种分割没有确保当数组被分割到足够小时递归会停止,
    因此递归深度可能会不必要地增加,导致栈溢出。*/
	//// 左边数组排成有序[left, mid - 1],左闭右闭
	//MergeSort(nums, left, mid - 1);
	//// 右边边数组排成有序[mid, right],左闭右闭
	//MergeSort(nums, mid , right);
	//// 将左右两个有序数列合并
	//SpecialMergeSort(nums, left, mid - 1, mid, right);

}

时间复杂度:平均 O( NlogN) 

额外空间:O(1)

6. 快速排序

思路:

① 取首元素作为pivot,i指向pivot,j指向尾元素

② j向左移动找比pivot小的数据,交换到i位置

③ i向右移动找比pivot大的数据,交换到j位置

④ 移动到i和j相遇,相遇的位置是pivot安放的位置

⑤ 递归pivot的左侧和右侧直到有序

代码:

// 快速排序:
void QuickSort(vector<int>& nums, int left, int right)
{
	// 不要写left == right,递归时小集合边界处理会栈溢出
	if (left >= right) return;

	int pivot = nums[left];
	int i = left;
	int j = right;

	while (i < j)
	{
		// 从右向左找小于等于枢轴的元素
		while (i < j && nums[j] > pivot) j--;
		if (i < j) nums[i++] = nums[j]; // 交换并移动i指针

		// 从左向右找大于等于枢轴的元素
		while (i < j && nums[i] < pivot) i++;
		if (i < j) nums[j--] = nums[i]; // 交换并移动j指针

		// 注意这里不能用MySwap交换,而是将数据填到i和j指向的位置
		// 注意填完数据后指针要移动
		//while (j > 0 && nums[j] > pivot) j--;
		//MySwap(nums[i], nums[j]);
		//nums[i] = nums[j];
		//while (i < right && nums[i] < pivot) i++;
		//MySwap(nums[i], nums[j]);
		//nums[j] = nums[i];
	}

	nums[i] = pivot;
	QuickSort(nums, left, i - 1);
	QuickSort(nums, i + 1, right);
}

7. 堆排序

思路:

分为两步,先建堆后排序

① 建堆:

从最后一个非叶子节点开始,从后往前构建大根堆

每次都把当前子树的根节点向下和左右孩子较大值进行比较和交换,直到遇到叶子节点

② 排序

把根上最大的元素依次交换到尾巴上,然后重新调整堆

 建堆
void BuildHeap(vector<int>& nums, int st, int ed)
{
	if (nums.empty() || st>= ed || st < 0 || ed + 1 > nums.size()) return;

	// 最后一个非叶子节点
	int no_leaf_index = (st + ed) / 2;

	// 从最后一个非叶子节点开始,从后往前构建子树
	for (int i = no_leaf_index; i >= st; --i)
	{
		// cur_index记录当前构建子树的根节点下标
		int cur_index = i;

		// 根节点没有移动到叶子节点上就一直比较,
		while (cur_index <= no_leaf_index)
		{
			// 节点序列0开始,左孩子 = 2 * 根节点 + 1 ,右孩子 = 2 * 根节点 + 2 
			// 节点序列1开始,左孩子 = 2 * 根节点 ,右孩子 = 2 * 根节点 + 1 
			int lchild_index = 2 * cur_index + 1;
			int rchild_index = 2 * cur_index + 2;

			// swap_index记录左右孩子中较大值的下标,注意可能没有右孩子,一定有左孩子
			int swap_index = lchild_index;
			if (rchild_index <= ed) swap_index = nums[lchild_index] > nums[rchild_index] ? lchild_index : rchild_index;

			// 交换的下标不能超出ed范围
			if (swap_index > ed) break;

			// 如果swap_index上的值比cur_index的大,交换节点,同时更新节点序号
			if (nums[swap_index] > nums[cur_index])
			{
				MySwap(nums[cur_index], nums[swap_index]);
				cur_index = swap_index;
			}
			else// 如果swap_index上的值比cur_index的小,跳出当前子树循环,进入下一个子树
			{
				break;
			}

		}
	}
}

// 堆排序:
void HeapSort(vector<int>& nums)
{
	// 1. 建堆
	BuildHeap(nums, 0, nums.size() - 1);

	// 2. 排序
	for (int i = nums.size() - 1; i > 0; i--)
	{
		// 交换
		MySwap(nums[0], nums[i]);
		// 调整
		BuildHeap(nums, 0, i - 1);
	}
}

时间复杂度:平均 O( NlogN) 

额外空间:O(1)

8. 总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值