冒泡/归并/插入/选择排序的稳定性与复杂度

文章详细介绍了冒泡排序和归并排序的算法实现,包括它们的时间复杂度和稳定性。冒泡排序为O(n²),适合小规模数据,是稳定的。归并排序采用分治策略,时间复杂度为O(nlogn),在处理大规模数据时更优,也是稳定的。文中还讨论了如何优化归并排序,如预先分配临时数组以减少内存开销,并提供了递归和非递归两种版本的实现。
摘要由CSDN通过智能技术生成

 先看表格,下面慢慢解释。

Bubble Sort——O(n²)——稳定

void bubbleSort(int * a, int n)
{
	bool isSorted = false;
	for (int i = n - 1; !isSorted && i > 0; --i) // 待排序范围
	{
		isSorted = true;
		for (int j = 0; j < i; ++j) // 从开头到无序的前一个位置
		{
			if (a[j] > a[j + 1])
			{
				std::swap(a[j], a[j + 1]);
				isSorted = false;
			}
		}
	}
	return;
}

冒泡排序,相邻两两交换,每次交换消去一个逆序对。一个序列最多有n²/2个逆序对,所以复杂度是O(n²)。两个相邻的data若一样,则不会被交换。所以冒泡排序是稳定的。

对于一个算法,补充如下思考。(来自参考资料1)

robustenss:应对退化(degeneracy)情况,比如n<=0,上述代码仍可正常返回。

重用性:对于float/char等类型,算法应能推广。见下代码。这是一个成员函数模板。

实际上,stl中的排序算法在algorithm中,独立于容器。是函数模板。

template <typename T>
void Vector<T>::bubbleSort(int lo, int hi)
{
	bool isSorted = false;
	for (int i = size - 1; !isSorted && i > 0; --i)
	{
		isSorted = true;
		for (int j = 0; j < i; ++j)
		{
			if (elem[j] > elem[j + 1])
			{
				std::swap(elem[j], elem[j + 1]);
				isSorted = false;
			}
		}
	}
	return;
}

此后我们的代码都写为模板的形式,Vector的定义见:

数据结构之vector的实现

Merge Sort——O(nlogn)——稳定

总结:

1. 出于对空间复杂度的考量,我们不是在每次merge时开辟临时数组,而是在归并排序的最外层函数(调整接口的那一层)开辟一个最大的临时数组,然后每次merge或递归时带着这个临时数组的地址作为参数。

2. 左闭右开区间大大的简化了表达式与思维量。

3. 分治法的复杂度分析(递推式)

4. 临时数组来回倒腾可以把already设置成lo

5. 所有排序都是,参数是xxxSort(int lo, int hi), 但我们将其映射从0开始排序,共hi-lo个元素

6.error:不允许使用默认参数 可能原因 ①函数重复②从右向左③声明时指定 非静态成员变量当默认参数虽然不会报这个错,但也不行

7. 提高效率:tmp数组和elem每趟归并从一个倒到另一个,下次再倒回来。

8. 非递归归并映射之后,不能再用merge了,应该再写一个,以newElem为待归并数据,或压根不用映射。

template <typename T>
void Vector<T>::mergeSort_rec(int lo, int hi) //统一接口,递归版归并排序
{
	T * tmp = new T[hi];
	_mergeSort(lo, hi, tmp);
	delete[]tmp;
	return;
}

template <typename T>
void Vector<T>::_mergeSort(int lo, int hi, T * tmp)
{
	if (hi - lo <= 1) return;
	else
	{
		_mergeSort(lo, (lo + hi) / 2, tmp);
		_mergeSort((lo + hi) / 2, hi, tmp);
		merge(lo, (lo + hi) / 2, hi, tmp);
	}
	return;
}

template <typename T>
void Vector<T>::merge(int lo, int mi, int hi, T * tmp)
{
	int left = lo, right = mi;
	int already = 0;
	while (left != mi && right != hi)
	{
		if (elem[left] <= elem[right])
		{
			tmp[already] = elem[left];
			++left;
			++already;
		}
		else
		{
			tmp[already] = elem[right];
			++right;
			++already;
		}
	}
	while (left != mi) tmp[already++] = elem[left++];
	while (right != hi) tmp[already++] = elem[right++];

	for (int i = lo; i < hi; ++i)
	{
		elem[i] = tmp[i - lo];
	}
	return;
}

template <typename T>
void Vector<T>::mergeSort_nonrec(int lo, int hi) //非递归版归并排序
{
	for (int i = 0; i < size; ++i) std::cout << elem[i] << " ";
	std::cout << std::endl;

	// 共 n 个元素
	int n = hi - lo; 

	// 归并用的临时数组
	T * tmp = new T[hi];

	//最长有序区间长度
	int length = 1; 

	//归并后的有序区间长度(不算尾巴)
	int doublelength = 2; 

	for (; length < n; length *= 2, doublelength *= 2) //每一躺归并
	{
		for (int i = 0; (i+1)*doublelength <= n; ++i)
		{
			merge(i*doublelength+lo, i*doublelength + length+lo, (i + 1)*doublelength+lo, tmp); //加lo是取消映射
		}
		// 尾巴
		if (n % doublelength > length) merge(n/doublelength*doublelength + lo, n / doublelength * doublelength+length + lo, n + lo, tmp);
		for (int i = 0; i < size; ++i) std::cout << elem[i] << " ";
		std::cout << std::endl;
	}

	delete[]tmp;
	return;
}

选择排序——O(n²)——稳定/不稳定?

稳定与否,取决于怎么移动元素。

每次选出最值,只一次交换,那就不稳定。

eg.

A 80 B 80 C 70 升序排列

第一趟 选出A,swap(A, C) 变为 C 70 B 80 A 80 

第二趟 选出B,序列不变,还是 C 70 B 80 A 80 排序完成

所以不稳定

有人说把大于号变成大于等于号,这样虽然在前缀中选出的最大值是最靠右的那一个,但是swap将原本最末位置的元素提到了前面,破坏了原有顺序,请看下例

A 90 B 80 C 70 D 20 E 20

第一趟选出B(因为用大于等于号,所以选出的是B不是A)E20 B 80 C 70 D 20 A 90

注意这样E就跑到D左边去了

第二趟:E20 D 20 C 70 B 80 A 90

第三趟、第四趟都是E20 D 20 C 70 B 80 A 90

还是不稳定

(来自我的知乎回答

基于链表或将所有元素左移,则稳定。

复杂度是O(n²)还是O(nlogn)受限于每趟选出最大值的方法,若用堆,则成为堆排序,不稳定,O(nlogn)。

所以背表格是没有用滴。但是“简单选择排序”的算法就是基于数组、扫描选最值、swap 所以是O(n²)且不稳定。

以下代码以基于List的选择排序为例。 数据结构List的定义请看此处

template <typename T>
void List<T>::selectionSort(listNode<T>*p, int n)
{
	// 找到有序后缀(注意,初始长度为0)
	listNode<T>* pos = p; //一会儿要在pos之前插入
	for (int i = 1; i <= n; ++i) pos = pos->succ;

	for (int rank = 0; rank < n; ++rank, pos = pos->pred) //有序后缀的长度
	{
		// 找前缀中最大值
		listNode<T> * tmp_max = Max(pos->pred, n - rank);

		// 插入后缀最前面
		insertBefore(tmp_max->data, pos);
		remove(tmp_max);
	}
}

// 从p开始往前找n个,这些data之中最大的,且最右的
template <typename T>
listNode<T>* List<T>::Max(listNode<T>* p, int n)
{
	listNode<T> * tmp_max = p;
	while (n-- && p != head)
	{
		if (p->data > tmp_max->data) tmp_max = p;
		p = p->pred;
	}
	return tmp_max;
}

插入排序——O(n²)——稳定

template <typename T>
void List<T>::insertionSort(listNode<T>*p, int n)
{
	for (int rank = 1; rank <= n; ++rank) //有序前缀的长度
	{
		listNode<T>* pos = search(p->data, p->pred, rank);
		insertAfter(p->data, pos);
		//p = p->succ;
		p = remove(p->pred);
	}
}
参考资料

邓俊辉 数据结构

陈越 数据结构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值