数据结构与算法分析 七、排序

在这里插入图片描述
练习地址:https://leetcode-cn.com/problems/sort-an-array/

前提

●算法的输入可以互换。每个算法接收一个含有元素的数组和一个包含元素个数的整数。
●传入的元素个数N是合法的,数据从0处开始
●假设<和>运算符都存在

冒泡排序

算法

 首先将第一个关键字和第二个关键字进行比较,若为逆序则将两个记录交换,然后比较第二个记录和第三个记录,依次类推,知道第n-1个记录和第n个记录的关键字进行比较为止。此过程称为第一趟起泡排序,其结果为使得关键字最大的记录被安置到最后一个记录的位置,然后进行第二趟排序。

代码

/*冒泡排序*/
void BubbleSort(vector<int>&vec, int N) {
	for (int i = 0; i < N; i++) {
		//N - i后面的都是排序好的大元素
		for (int j = 1; j < N - i; ++j)
			if (vec[j - 1] > vec[j])
				swap(vec[j - 1], vec[j]);
	}
}

分析

 冒泡排序的平均时间复杂度是O(n^2)

插入排序

算法

//插入排序由N-1趟(pass)排序组成。对与P = 1到N-1趟,插入排序保证0到P-1位置上的元素是已排序的
//在第P趟,为了将位置P上的元素向左移动到正确的位置上。先存储P上的元素,然后把比它大的元素都往右挪,直到有小于等于它的,
void InsertionSort(vector<int>&vec, int N) {
	for (int p = 1; p < N; p++) {
		int j;
		int tmp = vec[p];
		for (j = p; j > 0 && vec[j - 1] > tmp; j--)
			vec[j] = vec[j - 1];
		vec[j] = tmp;
	}
}

分析

●时间复杂度:2+3+4+…+N = O(N^2)
●输入已预先排序的时间复杂度:O(N),因为内层for循环总是立即终止。所以如果输入几乎被排序,那么插入排序将会运行的很快。

希尔排序(缩小增量排序)

希尔排序是冲破二次时间屏障的第一批算法之一。

思想

https://www.cnblogs.com/chengxiao/p/6104371.html

代码

 这里使用流行的增量序列:初始为N/2,每次/2,即希尔增量

void ShellSort(vector<int>&vec, int N) {
	//第一层循环:增量初始化为N/2,每次/2
	for (int inc = N / 2; inc > 0; inc /= 2) 
		//第二层循环:当前增量为inc,i从inc开始是因为i-inc要存在(即前面要有数来进行插入排序,就像插入排序从1开始)
		for (int i = inc; i < N; i++) {
			int tmp = vec[i];
			int j;
			//第三层循环:对当前的从i往前间隔为inc的数组进行插入排序,上面的插入排序即inc为1时的情况
			for (j = i; j >= inc && vec[j - inc] > tmp; j -= inc)
				vec[j] = vec[j - inc];
			vec[j] = tmp;
		}
}

分析

●最坏情况:
 使用希尔增量时希尔排序的最坏情形运行时间为O(N^2),Hibbard增量和Sedgewick增量序列的最坏情形比希尔增量要好。

堆排序

思想

利用之前的堆,先建堆,再DeleteMax。为了节省空间,将DeleteMax得到的值放到数组后面

代码

https://blog.csdn.net/qq_36573828/article/details/80261541

// 下滤
void percDown(vector<int>& vec, int i, int N) {
	int j = 2 * i + 1;
	while (j < N) {
		if (j + 1 < N && vec[j + 1] > vec[j])
			j++;
		if(vec[i] > vec[j])
			break;
		swap(vec[i], vec[j]);
		i = j;
		j = i * 2 + 1;
	}
}

void HeapSort(vector<int>& vec, int N) {
	// 从中间往前建堆
	for (int i = N / 2 - 1; i >= 0; i--)
		percDown(vec, i, N);
	// 将堆顶取出,放到后面,并将新的堆顶下滤
	for (int i = N - 1; i > 0; i--) {
		swap(vec[i], vec[0]);
		percDown(vec, 0, i);
	}
}

分析

●时间复杂度:
 建立N个元素的二叉堆花费O(N),执行N次DeleteMin操作,每次花费O(logN),总运行时间是O(NlogN)。
 所以堆排序花费O(NlogN)的时间,但是在实践中慢于使用Sedgewick增量序列的希尔排序。

●空间复杂度:O(N)
 增加了存储需求,这是一个弊端。从第二个数组拷贝回第一个 数组消耗O(N)时间,这不会显著影响运行时间。
 可以避免使用第二个数组,每次DeleteMin之后,堆缩小了1.因此位于堆中最后的元素可以用来存放刚刚删去的元素。
 所以采用大顶堆的话,最后数组将会为升序。

归并排序

思想

 如将8个元素的数组排序,可以递归地将前四个数据和后四个数据进行排序,然后将两部分合并。这是经典的分治思想。
在这里插入图片描述

代码

/*归并排序*/
//left_end等于right - 1,所以不用传入
void Merge(vector<int>&vec, vector<int>&tmp, int left, int right, int right_end) {
	int left_end = right - 1;
	int tmp_pos = left;				//遍历tmp
	int num = right_end - left + 1;	//总数
	while (left <= left_end && right <= right_end) {
		if (vec[left] <= vec[right])
			tmp[tmp_pos++] = vec[left++];
		else
			tmp[tmp_pos++] = vec[right++];
	}
	//有一个到底了
	while (left <= left_end)
		tmp[tmp_pos++] = vec[left++];
	while (right <= right_end)
		tmp[tmp_pos++] = vec[right++];
	//因为right_end没变,所以从right_end拷贝回vec
	for (int i = 0; i < num; i++, right_end--)
		vec[right_end] = tmp[right_end];
	
}
//将vec中的left到right排序
void MSort(vector<int>&vec, vector<int>&tmp, int left, int right) {
	int center;
	if (left < right) {
		center = left + (right - left) / 2;
		MSort(vec, tmp, left, center);
		MSort(vec, tmp, center + 1, right);
		Merge(vec, tmp, left, center + 1, right);
	}
}

//tmp的分析见下面
void MergeSort(vector<int>&vec, int N) {
	//在这里而不是递归中声明临时数组,递归中声明的话会声明很多个
	vector<int>tmp(N);
	MSort(vec, tmp, 0, N - 1);
}

分析

●时间复杂度:由T(N) = 2*T(N/2) + N可以得到T(N) = O(NlogN)

●缺点:很难用于主存排序,主要问题在于两个排序的表需要线性附加内存,还要花费时间将数据拷贝,放慢了排序的速度。所以人们大多使用快速排序来进行内部排序。

●tmp的作用:如果每个递归调用均局部声明一个临时数组,那么在任一时刻可能就有logN个临时数组处在活动期,这样会消耗很多内存。使用tmp在任一时刻只需要一个临时数组活动。

快速排序

思想

在这里插入图片描述

枢纽元的选取

错误方法
 直接选取第一个元素时,如果输入是预排序或者反序的,会产生劣质的分割,因为所有的元素不是都在S1就是都在S2。

随机选取
 指针方法比较安全,但是随机数的生成比较昂贵,减少不了算法其余部分的平均时间。

三数中值
 枢纽元的最好的选择是所有数的中值,但是这样代价昂贵。因此使用左端、右端和中心位置上三个元素的中值作为枢纽元

代码

/*快速排序*/
int partition(vector<int>&nums, int left, int right) {
	//int pivot = (left + right) / 2;
	int pivot = (rand() % (right - left + 1)) + left;
	swap(nums[pivot], nums[left]);
	int i = left, j = right, k = nums[left];
	//这里是挖坑法,还有一种交换法
	while(i < j){
            while(i < j && nums[j] >= k)
                j--;
            nums[i] = nums[j];
            while(i < j && nums[i] <= k)
                i++;
            nums[j] = nums[i];
        }
	//此时i = j
	nums[i] = k;
	return i;
}
// 快排
void quickSort(vector<int>&nums, int left, int right) {
	if(left >= right)
		return;
	int index = partition(nums, left, right);
	// 此时index左边<=nums[index],右边>=nums[index],再将左右分别排序就好
	quickSort(nums, left, index - 1);
	quickSort(nums, index + 1, right);
}

分析

小数组时
 小数组(N<=20),快速排序不如插入排序好,因为快速排序是递归的。所以在小数组时采用插入排序这种对小数组有效的排序算法。

时间复杂度
●最坏情况:枢纽元始终是最小元素。T(N) = T(N - 1) + cN,N > 1,时间复杂度为O(N^2)

●最好情况:枢纽元正好位于中间。T(N) = 2T(N/2) + cN,时间复杂度为O(NlogN)

应用

快速选择
 用于求得第k个最大(最小)元
https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/

桶排序

思想

 桶排序将输入数据的区间均匀分成若干份,每一份称作“桶”。分别对每一个桶的内容进行排序,再按桶的顺序输出则完成排序。

代码

/*桶式(链表)排序*/

int find_max(vector<double>vec, int N) {
	int max_val = vec[0];
	for (int i = 1; i < N; i++)
		max_val = max(max_val, int(vec[i]));
	return max_val;
}

//输入为大于0的double类型数据
void BucketSort(vector<double>&vec, int N) {
	//找出最大值,根据最大值得到桶数
	int size = find_max(vec, N);
	vector<list<double>>list_vec(size + 1);
	//把数据放到相应的桶
	for (int i = 0; i < N; i++)
		list_vec[int(vec[i])].push_back(vec[i]);
	//每个桶内部排序
	for (int i = 0; i <= size; i++)
		list_vec[i].sort();
	int j = 0;
	//遍历每个桶,将数据拷贝回去
	for(int i = 0; i <= size; i++)
		while (list_vec[i].size() > 0) {
			vec[j++] = list_vec[i].front();
			list_vec[i].pop_front();
		}
}

分析

●桶排序是稳定的
●桶排序是常见排序里最快的一种,比快排还要快…大多数情况下
●桶排序非常快,但是同时也非常耗空间,基本上是最耗空间的一种排序算法

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值