排序算法(冒泡、选择、插入、快排、堆排、归并)

冒泡排序

void bubbleSort(vector<int>& nums)
{
	int n = nums.size();
	for (int i = 0; i < n - 1; ++i)
	{
		for (int j = 0; j < n - 1 - i; ++j)
		{
			if (nums[j] > nums[j + 1])
				swap(nums[j], nums[j+1]);
		}
	}
}

时间复杂度 O(n2)

原理

遍历 n-1 轮,每轮遍历未排序部分,通过比较相邻元素并将较大值交换到右侧,来将当前未排序数组的最大值移动到未排序数组的尾部。

通过添加标志位的方式,可以让最好复杂度变成 O(n) ,通过添加标志位,使得冒泡排序在排序已经完成的情况下,退出循环,这样在数组已经排序好的情况下,只会进行一次遍历。

void bubbleSort(vector<int>& nums)
{
	bool swapped;
	int n = nums.size();
	for (int i = 0; i < n - 1; ++i)
	{
		swapped = false;
		for (int j = 0; j < n - 1 - i; ++j)
		{
			if (nums[j] > nums[j + 1])
			{
				swap(nums[j], nums[j + 1]);
				swapped = true;
			}
		}
		if (!swapped) break;
	}
}

选择排序

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;
			}
		}
		swap(nums[i], nums[minIndex]);
	}
}

时间复杂度 O(n2)

原理

遍历 n-1 轮,每轮遍历数组未排序部分,寻找最小值的索引,将其交换到未排序部分的首部。

插入排序

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;
	}
}

时间复杂度 O(n2)

原理
将数组分为已排序未排序两部分,逐个将未排序元素插入到已排序部分的正确位置。

image

快速排序

void quickSort(vector<int>& nums, int left, int right)
{
	if (left >= right) return;  // 终止条件
	int pivot = nums[left];
	int begin = left, end = right;
	while (begin < end)
	{
		while (begin < end && nums[end] > pivot)
			end--;
		if (begin < end)
		{
			nums[begin] = nums[end];
		}
		while (begin < end && nums[begin] <= pivot)
			begin++;
		if (begin < end)
		{
			nums[end] = nums[begin];
		}
	}
	nums[begin] = pivot;
	quickSort(nums, left, begin - 1);
	quickSort(nums, begin + 1, right);
}

时间复杂度为 O(nlogn),最坏情况时间复杂度为 O(n2)

原理

  1. 选择基准值:从数组中选择一个元素作为基准
  2. 分区操作:排列数组使得小于基准的元素位于基准前面,大于基准的元素位于基准后面
  3. 递归排序:递归地对小于基准的子数组排序,递归地对大于基准的子数组排序

为什么最坏情况时间复杂度是 O(n²)?

当每次分区后,子数组极度不平衡时,例如:

  • 数组已完全有序(升序或降序)。
  • 每次选择的枢轴是当前子数组的最小或最大元素

此时每次分区只能将数组分为 1 个元素(枢轴)n-1 个元素,导致递归树退化为链表,从而递归深度为n,而每层排序的时间复杂度是O(n)。

堆排序

堆排序分成两部分:维护堆结构和使用维护好的堆排序。

堆结构有大根堆和小根堆,大根堆的父节点大于左右子节点,小根堆的父节点小于左右子节点。因此大根堆的根节点是堆中的最大值,小根堆的根节点是堆中的最小值。

核心思想:

  1. 构建堆
  2. 反复将堆顶元素与堆尾元素交换
  3. 维护剩余元素为堆结构
  4. 重复直到整个序列有序

以下以大根堆为例:

void keepHeap(vector<int>& nums, int n, int i)
{
	int lChild = 2 * i + 1;
	int rChild = 2 * i + 2;
	int parent = i;
	if (lChild < n)
	{
		parent = nums[lChild] > nums[parent] ? lChild : parent;
	}
	if (rChild < n)
	{
		parent = nums[rChild] > nums[parent] ? rChild : parent;
	}
	if (parent != i)
	{
		swap(nums[parent], nums[i]);
		keepHeap(nums, n, parent);
	}
}

void heapSort(vector<int>& nums)
{
int n = nums.size();
int i;
for (i = n/2-1; i >= 0; --i)
{
keepHeap(nums, n, i);
}
for (i = n-1; i>=0; --i)
{
swap(nums[0], nums[i]);
keepHeap(nums, i, 0);
}
}

时间复杂度 O(nlogn),最坏时间复杂度 O(nlogn)

堆排序实质是将数组维护成二叉树结构,以下是大根堆排序过程图解:

07_数据结构与算法排序算法 assets02_堆排序.assetsimage-20240920185705168

归并排序

归并排序分成两步:分组和合并。

核心思想:

  1. 分组:将数组递归拆分成左右两个子数组
  2. 合并:当子数组长度为1时,认为其有序,不再分组。并将两个有序数组合并成一个有序数组
void merge(vector<int>& nums, int left, int mid, int right)
{
	// 合并两个有序数组,[left, mid] [mid+1, right]
	vector<int> tmp(right - left + 1);
	int i = left, j = mid+1;
	int index = 0;
	while (i < mid + 1 && j <= right)
	{
		if (nums[i] < nums[j])
			tmp[index++] = nums[i++];
		else
			tmp[index++] = nums[j++];
	}
while (i &lt; mid + 1)
{
	tmp[index++] = nums[i++];
}

while (j &lt;= right)
{
	tmp[index++] = nums[j++];
}

index = 0;
for (i = left; i &lt;= right; ++i)
{
	nums[i] = tmp[index++];
}

}

void mergeSort(vector<int>& nums, int left, int right)
{
if (left >= right) return; // 边界条件
// 分组
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 合并
merge(nums, left, mid, right);
}

最优/平均/最差时间复杂度:O(nlogn)

  • 原因:

    • 递归深度:log n
    • 每层合并操作:O(n)

算法图解:

image

posted @ 2025-05-31 14:29  重光拾  阅读(32)  评论(0)    收藏  举报