左神课程笔记——第三节课:详解桶排序以及排序内容大总结

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


上节课补充

快排空间复杂度O(logN),最差情况是O(N),需要调用系统栈记录划分值所在的位置,如果每次选取的划分值都在两端,则栈有N层,空间复杂度就是O(N);如果每次选取的划分值都在中间,则栈有logN层,空间复杂度为O(logN)。通过随机选取划分值可以使得空间复杂度为O(logN),类似于时间复杂度的分析。
在这里插入图片描述


1.堆–>堆排序

逻辑上是完全二叉树(二叉树从左往右依次变满)。将完全二叉树的节点与数组中从下标0开始的元素依次对应:数组中下标i对应的左孩子在数组中的下标是2i+1、右孩子是2i+2,对应的父节点在数组中的下标是(i-1)/2(0的父节点还是自己)。
在这里插入图片描述
大根堆:完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
小根堆:完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
heapInsert:新插入的节点与其父节点比较,如果比父节点大,则交换,然后再与交换后的父节点(i-1)/2比较,直到不比父节点大或者当前节点已经是整棵树的根节点为止。时间复杂度为O(logN),向上走一个高度。
在这里插入图片描述
代码实现:

void heapInsert(vector<int>& arr, int index) {
	while (arr[index] > arr[(index - 1) / 2]) {//包含了当前节点是0根节点的判断
		swap(arr[index], arr[(index - 1) / 2]);
		index = (index - 1) / 2;
	}
}

heapify:用一个变量存储根节点(最大值),然后将最后一个节点的值移到根节点,heapSize–(等同于将最后一个节点剔除出堆)。这时整个完全二叉树可能不是堆,需要从头节点开始,选择左孩子和右孩子中的最大值,如果比最大值小,则与这个孩子交换,直到比当前位置的左孩子右孩子的值都大,或者当前位置没有左孩子(当然也不可能有右孩子)。时间复杂度为O(logN),向上走一个高度。
在这里插入图片描述
heapify是从上往下、heapInsert是从下往上。如果更改了堆中的某个节点的值,如果比原来值变大了,则向上heapInsert;如果比原来值变小了,则向下heapify;如果不告诉是变大还是变小,则可以都试一下,两个只能中一个。
代码实现:

void heapify(vector<int>& arr, int index, int heapSize) {//从index位置开始做heapify
	int left = 2 * index + 1;//左孩子下标
	while (left < heapSize) {//左孩子下标存在的条件
		//将两个孩子值大的下标赋给largest(要判断是否存在右孩子)
		int largest = (left + 1 < heapSize) && (arr[left + 1] > arr[left]) ? left + 1 : left;
		//当前节点与较大孩子节点比较
		largest = arr[index] < arr[largest] ? largest : index;
		if (largest == index) {
			break;
		}
		swap(arr[index], arr[largest]);
		index = largest;
		left = 2 * index + 1;
	}
}

堆排序:heapSize=0开始,每次将堆的范围扩大1,相当于每次heapInsert一个元素,最后将整个数组变成一个堆;然后将堆中的最后一个元素与第一个元素(最大值)交换,并将heapSize–(也就是将最后一个元素与堆断开联系,该值已经来到排好序的位置);从根节点做heapify,再次调整成大根堆;继续上述过程,直到heapSize==1
在这里插入图片描述
代码实现:

void heapSort(vector<int>& arr) {
	if (arr.size() < 2) {
		return;
	}
//	for (int i = 0; i < arr.size(); i++) {//O(N)
//		heapInsert(arr, i);//O(logN)
//	}
	for (int i = arr.size() - 1; i >= 0; i--) {
		heapify(arr, i, arr.size());
	}
	int heapSize = arr.size();
	swap(arr[0], arr[--heapSize]);
	while (heapSize > 0) {//O(N)
		heapify(arr, 0, heapSize);//O(logN)
		swap(arr[0], arr[--heapSize]);
	}
}

时间复杂度:O(NlogN),空间复杂度:O(1)
给定数组所有元素的情况下也可以有更好的方法进行堆化:依次从叶子节点从右向左分别做heapify。时间复杂度:
在这里插入图片描述
在这里插入图片描述
例:已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
解:将数组的前k+1个元素调整成小根堆,则堆中的第一个元素所在位置就是排好序的位置;然后将堆顶弹出,数组中的后一个元素添加近堆中,再调整成小根堆;重复上述过程,直到将最后一个数组元素也加入到堆中;最后依次将堆顶元素弹出放到数组中,整个数组有序。
在这里插入图片描述
时间复杂度:O(N*logk)。
代码实现:

void sortedArrDistanceLseeK(vector<int>& arr, int k) {
	priority_queue<int, vector<int>, greater<int>>heap;
	int index = 0;
	for (; index <= k; index++) {
		heap.push(arr[index]);
	}
	int i = 0;
	for (; index < arr.size(); i++, index++) {
		arr[i] = heap.top();
		heap.pop();
		heap.push(arr[index]);
	}
	while (!heap.empty()) {
		arr[i++] = heap.top();
		heap.pop();
	}
}

C++提供priority_queue容器来实现堆(但有些时候要手写堆才能做到高效),priority_queue用法如下:
1)包含头文件:#include
2)定义:priority_queue<Type, Container, Functional>
a)Type:数据类型
b)Container:容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用list。STL里面默认用的是vector)
c)Functional:比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆
d)小根堆:priority_queue <int,vector,greater > q;
大根堆:priority_queue <int,vector,less >q;
3)基本操作:和队列相同
e)top:访问队头元素
f)empty:队列是否为空
g)size:返回队列内元素个数
h)push:插入元素到队尾 (并排序)
i)emplace:原地构造一个元素并插入队列
j)pop:弹出队头元素
k)swap:交换内容

2.priority_queue多种自定义类型排序

(1)在自定义类型中重载<、>运算符,重载函数一定要声明const类型

//1.自定义类中重载比较运算符:< >
class Node01 {
public:
	int size;
	int price;
	//重载<运算符
	bool operator<(const Node01& b) const {//注意此处的const是必须的
		if (this->size == b.size) {
			return this->price < b.price;
		}
		return this->size < b.size;
	}
	//重载>运算符
	bool operator>(const Node01& b) const {
		if (this->size == b.size) {
			return this->price > b.price;
		}
		return this->size > b.size;
	}
};
void test01() {
	//priority_queue<Node01> pq;//默认是大根堆
	priority_queue<Node01, vector<Node01>, greater<Node01>> pq;//小根堆
	pq.push(Node01{ 1, 5 });
	pq.push(Node01{ 5, 1 });
	pq.push(Node01{ 1, 3 });
	pq.push(Node01{ 3, 5 });
	while (!pq.empty()) {
		cout << pq.top().size << " " << pq.top().price << endl;
		pq.pop();
	}
}

(2)使用函数对象(仿函数),直接将函数对象类当作priority_queue的第三个参数传给它

//2.使用函数对象(仿函数)
class Node02 {
public:
	int size;
	int price;
};
class Cmp {
public:
	bool operator()(const Node02& a, const Node02& b) {
		return a.size == b.size ? a.price > b.price:a.size > b.size;//小根堆
	}
};
void test02() {
	priority_queue<Node02, vector<Node02>, Cmp> pq;//小根堆
	pq.push(Node02{ 1, 5 });
	pq.push(Node02{ 5, 1 });
	pq.push(Node02{ 1, 3 });
	pq.push(Node02{ 3, 5 });
	while (!pq.empty()) {
		cout << pq.top().size << " " << pq.top().price << endl;
		pq.pop();
	}
}

(3)使用lambda表达式,需要使用decltype,同时将lambda表达式对象传入优先队列实例中

//3.使用lambda表达式
class Node03 {
public:
	int size;
	int price;
};
void test03() {
	auto cmp = [](const Node03& a, const Node03& b) {
		return a.size == b.size ? a.price > b.price:a.size > b.size;
	};//lambda表达式是一个表达式,这里必须要有";"
	priority_queue<Node03, vector<Node03>, decltype(cmp)> pq(cmp);//小根堆
	pq.push(Node03{ 1, 5 });
	pq.push(Node03{ 5, 1 });
	pq.push(Node03{ 1, 3 });
	pq.push(Node03{ 3, 5 });
	while (!pq.empty()) {
		cout << pq.top().size << " " << pq.top().price << endl;
		pq.pop();
	}
}

(4)使用函数指针:将一个bool类型的函数当作参数传给priority_queue,与lambda表达式类似

//4.使用函数指针
class Node04 {
public:
	int size;
	int price;
};
bool cmpFun(const Node04& a, const Node04& b) {
	return a.size == b.size ? a.price > b.price:a.size > b.size;
}
void test04() {
	priority_queue<Node04, vector<Node04>, decltype(&cmpFun)> pq(&cmpFun);//小根堆
	pq.push(Node04{ 1, 5 });
	pq.push(Node04{ 5, 1 });
	pq.push(Node04{ 1, 3 });
	pq.push(Node04{ 3, 5 });
	while (!pq.empty()) {
		cout << pq.top().size << " " << pq.top().price << endl;
		pq.pop();
	}
}

3.桶排序

之前的所有排序都是基于比较的排序,桶排序是基于数据状况的排序,应用范围没有基于比较的排序广。
桶排序中的计数排序参见《剑指offer专项突破》P208。
基数排序:以十进制为例,准备10个容器(桶),编号分别为0…9。首先按照数组中数据的个位从左到右放在对应编号的桶中,然后将数据从左往右依次从桶中取出(先进先出原则);然后按照数据的十位重复上述过程,直到数据的最大位,此时再取出的数据即为排好序的数据。
在这里插入图片描述
代码分析:用一个函数maxbits求数组中元素的最大十进制位;准备一个与原数组等规模的空间bucket;一共要出桶入桶的次数等于最大十进制位(外层循环);外层循环内部分析:
用一个长度为10的数组count记录每个数的个位数字出现的频次:
在这里插入图片描述
然后将count变成前缀和数组:
在这里插入图片描述
现在count每个元素的值的含义是个位数字小于等于下标的数字的个数;然后将原数组从右往左遍历,每遍历到一个数字num求其对应位在count的值,该值减一得到的数就是num应该放到bucket中的位置(注意还要将count在此位置的值减一),最后再将bucket的值赋值给原数组。至此完成一次入桶处桶的操作。
在这里插入图片描述
代码实现:

int maxbits(vector<int>& arr) {
	int maxNum = INT_MIN;
	for (int num : arr) {
		maxNum = max(num, maxNum);
	}
	int res = 0;
	while (maxNum != 0) {
		maxNum /= 10;
		res++;
	}
	return res;
}

int getDigit(int num, int d) {//求num第d位上的数字,d=1对应个位
	for (int i = 1; i < d; i++) {
		num /= 10;
	}
	return num % 10;
}

void radixSort(vector<int>& arr, int l, int r, int digit) {//对数组的l…r进行排序,digit是数组中最大位数
	int radix = 10;//10进制数
	vector<int>bucket(r - l + 1);//辅助空间
	for (int d = 1; d <= digit; d++) {//有多少位就进出多少次
		vector<int>count(radix);
		for (int i = l; i <= r; i++) {
			int j = getDigit(arr[i], d);
			count[j]++;
		}
		for (int i = 1; i < count.size(); i++) {
			count[i] = count[i] + count[i - 1];
		}
		for (int i = r; i >= l; i--) {
			int j = getDigit(arr[i], d);
			bucket[count[j] - 1] = arr[i];
			count[j]--;
		}
		for (int i = l, j = 0; i <= r; i++, j++) {
			arr[i] = bucket[j];
		}
	}
}

void radixSort(vector<int>& arr) {
	if (arr.size() < 2) {
		return;
	}
	radixSort(arr, 0, arr.size() - 1, maxbits(arr));
}

4.排序算法的稳定性

稳定性:相同的元素排序之后是否保持相对次序不变。基础类型没用,非基础类型有用(自定义数据类型)
选择排序:不稳定
在这里插入图片描述
冒泡排序:稳定(相等的时候不能交换)
在这里插入图片描述
插入排序:稳定(相等的时候不能交换)
归并排序:稳定(双指针指向的数字相等时,应先copy左边的;小和问题是先copy右边的,因此得到的有序数组是不稳定的)
在这里插入图片描述
快排:不稳定
在这里插入图片描述
在这里插入图片描述
堆排:不稳定
在这里插入图片描述
计数排序和基数排序:稳定(因为和比较无关)

5.总结

在这里插入图片描述
一般选择快排(最快),用空间限制选堆排,需要稳定时选归并。目前没有找到时间复杂度O(NlogN),额外空间复杂度O(1),又稳定的排序。
常见的坑:
(1)归并排序的额外空间复杂度可以变成O(1),“归并排序内部缓存法”,但是就不稳定了,不如选择堆排
(2)“原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O(N^2),不如用插入
(3)快速排序可以做到稳定,但是非常难,不需要掌握,可以搜“01 stable sort”,空间复杂的变成O(N),不如用归并
(4)所有的改进都不重要,因为目前没有找到时间复杂度O(N
logN),额外空间复杂度O(1),又稳定的排序。
(5)有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官。经典快排partition的思想可以做到奇数放左边,偶数放右边,但是快排不稳定,原始的相对次序改变了
工程上对排序的改进:
(1)充分利用O(N*logN)和O(N^2)排序各自的优势
(2)稳定性的考虑:基础类型用快排,自定义类型用归并

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值