排序算法笔记

排序

1. 选择排序

Selection Sort

定义最小值的游标,循环比较,跳着把比起小的值交换到左边

不稳定,时间复杂度最好最坏平均都是 O(n^2),空间复杂度 O(1)

基本不用

void myprint(int arr[], int len)
{
	for (int i = 0; i < len; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
}

void myswap(int arr[], int i, int j)
{
	int temp;
	temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
}

class Solution 
{
public:
	void selection_sort(int arr[], int len)	{
		for (int j = 0; j < len-1; j++) {  // 不妨把轮数写大一点,测试的时候降低
			int minPos = j;
			for (int i = j+1; i < len; i++)  { // i是index,最大值取len-1
				minPos = arr[minPos] > arr[i] ? i : minPos;  
			}
			myswap(arr, minPos, j);
			cout << "第" << j+1 << "轮排序:" << endl;
			myprint(arr, len);
		}
	}
};

int main()
{
	Solution s;
	int Arr[] = { 2,0,22,51,6,324,-7,6,5,543,23 };
	int len = sizeof(Arr) / sizeof(Arr[0]);
    
	cout << "排序前:" << endl;
	myprint(Arr, len);
	cout << endl;

	s.selection_sort(Arr, len);

	system("pause");
	return 0;
}

C++注意事项:数组传入函数时,尽量传入它的length,在函数内部计算 sizeof(arr) 会出错

debug trick:边界条件若不好确定,等写完之后在确定

​ 打印中间结果,减少功能模块来定位bug

2. 冒泡排序

Bubble Sort

从左到右,比较相邻两个数字,大的交换到右边,每轮循环找出一个最大值

相邻两个交换,不跳跃,稳定排序。最优时间复杂度 O(1),最坏 O(n^2),平均 O(n^2),空间复杂度 O(1)

class Solution
{
public:
	void bubble_sort(int arr[], int len) {
		cout << "排序前:" << endl;
		myprint(arr, len);
		cout << endl;

		int count = 0;
		for (int j = 0; j < len - 1; j++) {
			if (arr[j] < arr[j + 1]) count += 1;
		}
		if (count == len - 1) {
			cout << "第" << 0 << "轮:" << endl;
			myprint(arr, len);
			return;
		}					// 判断事前是否已经排好

		for (int j = 0; j < len - 1; j++) {
			for (int i = 0; i < len - 1 - j; i++) {
				if (arr[i] > arr[i + 1])	myswap(arr, i, i + 1);
			}
			cout << "第" << j+1 << "轮:" << endl;
			myprint(arr, len);
		}
	}
};
3. 插入排序

Insert Sort

从第二个数开始,向前比较若比其小,则交换

最优时间复杂度 O(n),最坏 O(n^2), 空间复杂度 O(1),稳定

对基本有序的数组最好用

class Solution
{
public:
	void insert_sort(int arr[], int len)	{
		cout << "排序前:" << endl;
		myprint(arr, len);
		for (int i = 0; i < len - 1; i++)	{
			for (int j = i + 1; j > 0 && arr[j] < arr[j-1]; j--)  // 前向交换,所以j--
                myswap(arr, j, j-1);
			cout << "第" << i+1 << "轮:" << endl;
			myprint(arr, len);
		}
	}
};
4. 希尔排序

Shell Sort

指定一个gap值,每次缩小两倍,按照gap值划分等区间,在各自区间对应次序位置上排序,直到 gap=1

考虑完两个gap之后,再追加一个gap

跳着拍,不稳定排序,平均时间复杂度 O(n^1.3),空间复杂度 O(1)

升级版的插入排序

class Solution
{
public:
	void shell_sort(int arr[], int  len)	{
		cout << "排序前:" << endl;
		myprint(arr, len);
		cout << endl;
		for (int gap = len/2; gap > 0; gap /= 2)	{
			for (int i = gap; i < len; i++)
				for (int j = i; j > gap - 1 && arr[j - gap] > arr[j]; j -= gap)
					myswap(arr, j, j - gap);
			cout << "gap = " << gap << endl;
			myprint(arr, len);
		}
	}
};
5. 归并排序

Merge Sort

用到了递归的概念:引自 @九章算法,https://www.zhihu.com/question/31412436

迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值,因此迭代是从前往后计算的。

递归则是一步一步往前递推,直到递归基础,寻找一条路径, 然后再由前向后计算。迭代是从前往后计算的,而递归则是先从后往前推,然后再由前往后计算,有“递”又有“归”。

通俗来讲:引自@lishichengyan

一个小朋友坐在第10排,他的作业本被小组长扔到了第1排,小朋友要拿回他的作业本,可以怎么办?

他可以拍拍第9排小朋友,说“帮我拿第1排的本子”,而第9排的小朋友可以拍拍第8排小朋友,说“帮我拿第1排的本子”…如此下去,消息终于传到了第1排小朋友那里,于是他把本子递给第2排,第2排又递给第3排…终于,本子到手啦!

排序时:

先假定两截数组已经有序,merge

  1. ​ 定义左右中游标和临时等长数组
  2. ​ 在两截中依次比较,把小的复制到临时数组
  3. ​ 把遗留下来的直接贴到临时数组

在sort中实现递归

  1. ​ 写出递归基础条件,只有一个元素时结束递
  2. ​ 对输入的边界中间劈开,左右sort排序
  3. ​ 调用merge

完整数组是“最后一个小朋友”,基础条件是“第一个小朋友”,基础条件排好序 “拿到作业”,把排好序的往后传,直到传到完整数组,排好序

JAVA、Python对对象的排序都是归并排序,为了稳定度

class Solution
{
public:
	void merge(int arr[], int LeftPtr, int RightPtr, int RightBound) // 假定两边已经排好顺序
	{
		int i = LeftPtr; // 指在前半截数组第一个位置, 可取[0, mid]
		int j = RightPtr; // 指在后半截数组第一个位置, 可取[mid+1, RightBound]
        int  mid = RightPtr - 1;  // 都是下标index
        
		int * temp;
		temp = new int[RightBound - LeftPtr + 1];  // 创建临时等长数组
		int k = 0;   // 指在temp第一个位置

		while (i <= mid && j <= RightBound)  // 把小的复制到temp 
			temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++]; 
        	// 这里的i, j, k不是循环内局部变量,可以存储变化

		// 把遗留下来的元素全部粘贴下来
		while (i <= mid)	temp[k++] = arr[i++];
		while (j <= RightBound)	    temp[k++] = arr[j++];

		for (int m = 0; m < RightBound - LeftPtr + 1; m++)	arr[LeftPtr+m] = temp[m];
        
        delete temp;
		temp = NULL;
	}

	void sort(int arr[], int LeftPtr, int RightBound)
	{ 
        // 只有一个元素不排序,也是递归的基础条件
		if (LeftPtr == RightBound) return;   
		// 分成两半
		int mid = (LeftPtr + RightBound) / 2;
		// 左边排序
		sort(arr, LeftPtr, mid);
		// 右边排序
		sort(arr, mid+1, RightBound);

		merge(arr, LeftPtr, mid+1, RightBound);
	}
};

int main()
{
	Solution s;
	int arr[] = { 15,14,13,12,11,10,9,8,7,6,5,4,3,2,1 };
	myprint(arr, 15);

	s.sort(arr, 0, 14);
	myprint(arr, 15);

	system("pause");
	return 0;
}

C++在函数体中用变量定义数组的长度(VS2017):

		int num;
		int * temp;
		temp = new int[num];
		"""…………"""
		delete temp;
		temp = NULL;

时间复杂度 O(nlogn),空间复杂度 O(n),稳定排序

6. 快速排序

Quick Sort

选出轴(基准)

  1. ​ 定义初始轴的位置(最后)
  2. ​ 从左边找比轴大的index,从右边找比轴小的index
  3. ​ 把大的值和小的值交换
  4. ​ 把轴和大的值交换
  5. ​ 返回轴的位置

递归

  1. ​ 写出基础条件
  2. ​ 定义轴的位置
  3. ​ 轴左边排序
  4. ​ 轴右边排序

平均时间复杂度 O(nlogn),空间复杂度 O(logn),不稳

int partition(int arr[], int leftBound, int rightBound)	{
	int pivot = arr[rightBound];  // 找到基准值
	int left = leftBound;
	int right = rightBound - 1; // 基准值前一个作为右边界
	while (left <= right)	{   
		while (left <= right && arr[left] <= pivot) left++;  // 从左边找比pivot大的index
		while (left <= right && arr[right] > pivot) right--;  // 从右边找比pivot小的index
		if (left < right) myswap(arr, left, right);
	}
	myswap(arr, left, rightBound);  // 再把轴和大的交换
	return left;  // 返回轴的位置
}

class Solution
{
public:
	void quick_sort(int arr[], int leftBound, int rightBound)	{
		if (leftBound >= rightBound) return;  // 一个元素不用排,基础条件
		int mid = partition(arr, leftBound, rightBound);  // 返回轴的位置
		quick_sort(arr, leftBound, mid - 1);
		quick_sort(arr, mid + 1, rightBound);
	}
};
7. 计数排序

Counting Sort

是桶排序的一种变式,非比较排序,适用于数据量大,但是取值范围小的场景

比如:快速得知高考名次;数万名员工 年龄排序

  1. 构造一个用于存放待排序数组下标值个数的数组 temp,注意是个数!
  2. ​ 构造一个累加数组存放各个桶最后一个元素在原数组的index值,实际上是index+1,因为temp统计的是各个桶里的个数
  3. ​ 构造结果排序数组,从原数组的最后一位逆向读取,根据adding放置到合适位置

时间复杂度 O(n+k),空间复杂度O(n+k),由于adding的存在所以是稳定排序

class Solution {
public:
	void counting_sort(int arr[], int len) {
		int index_max = 10;  // 待排序数组下标范围大小

		int * temp;
		temp = new int[index_max];
		for (int i = 0; i < index_max; i++)
			temp[i] = 0;
		for (int i = 0; i < len; i++)
			temp[arr[i]]++;

		int * adding;
		adding = new int[index_max];
		adding[0] = temp[0];
		for (int i = 1; i < index_max; i++) adding[i] = temp[i] + adding[i - 1];
		// adding数组中的每一个元素都代表了对应到temp数组中相应index表示的数在待排序数组中的末位

		int * new_arr;
		new_arr = new int[len];
		for (int i = len - 1; i >= 0; i--) {
			new_arr[adding[ arr[i] ] - 1] = arr[i];
			adding[arr[i]]--;
		}

		cout << "排序后: " << endl;
		myprint(new_arr, len);

		delete temp;
		delete adding;
		temp = NULL;
		adding = NULL;
	}
};

其中比较关键的操作是:

		for (int i = len - 1; i >= 0; i--) {
			new_arr[adding[ arr[i] ] - 1] = arr[i];  // adding[ arr[i] ] - 1 才是下标值
			adding[arr[i]]--;
		}
8. 基数排序

Radix Sort

非比较排序,桶排序的一种,多关键字排序 (个位、十位、百位)

返回数组中最大数的位数

int maxValue(int arr[], int len) {
	int max = 0;
	int maxvalue = 1;
	for (int i = 0; i < len; i++)
		max = max > arr[i] ? max : arr[i];

	for (int j = 0; j < 10; j++) {
		int div = pow(10, j);
		if (max / div != 0) 
			maxvalue++;
	}
	return maxvalue;
}

分别按照个位大小,十位大小,百位大小……等排序

class Solution {
public:
	void radix_sort(int arr[], int len, int maxvalue) {
		int num = len;
		int * count;
		count = new int[num];

		int number;
		for (int j = 0; j < maxvalue; j++)
		{
			int * adding;
			adding = new int[10];
			int * result;
			result = new int[num];

			for (int i = 0; i < 10; i++)  
				// 要清空,不然个数的也会留到十位数上
				count[i] = 0;

			int div = pow(10, j);   // 统计各个桶里有多少
			for (int i = 0; i < len; i++) {
				number = arr[i] / div % 10;
				count[number]++;
			}     

			adding[0] = count[0];
			for (int i = 1; i < 10; i++)
				adding[i] = adding[i - 1] + count[i];

			for (int i = len - 1; i >= 0; i--) {
				result[adding[(arr[i] / div) % 10] - 1] = arr[i];
				adding[(arr[i]/div) % 10]--;  // 排放一个,桶里就会少一个
			}

			for (int i = 0; i < len; i++)  // 先按照一个位的排
				arr[i] = result[i];

            myprint(result, len);

			delete result;
			delete adding;
			result = NULL;
			adding = NULL;
		}
	}
};
9. 堆排序

Heap Sort

  1. ​ 用到二叉树的思想,按照数组的下标排成二叉树

  2. ​ 把无序堆排成大根堆,依照从右到左,从上到小,比较左右叶子和父节点的大小,把最大的置顶

  3. ​ 把堆顶的数和最右下角的叶子交换,完成一次有序元素的抽取,并让数组长度减一

  4. ​ 循环2、3步,直到数组的长度等于1

一、由无序堆变成大根堆

最后一个带有叶子的父节点在数组中的下标为:
l a s t F a t h e r i n d e x = c e i l ( [ s t a r t + e n d ] / 2 ) − 1 lastFather_{index} = ceil([start + end]/2) - 1 lastFatherindex=ceil([start+end]/2)1
start 是数组的起始下标(为0),end 是数组最后一个位置的下标

由上式也可以推断出,当数组长度为偶数时,
l a s t F a t h e r i n d e x = [ s t a r t + e n d ] / 2 − 1 lastFather_{index} = [start + end]/2 - 1 lastFatherindex=[start+end]/21
长度为奇数时,
l a s t F a t h e r i n d e x = [ s t a r t + e n d ] / 2 lastFather_{index} = [start + end]/2 lastFatherindex=[start+end]/2
而父节点拿到后,其左叶子的下标为 father_index * 2 + 1,右叶子为 father_index * 2 + 2

void maxHeap(int arr[], int end)
{
	// 1 计算出堆最后一个父节点的下标
	int lastFather = (0 + end) % 2 == 0 ? (0 + end) / 2 - 1 : (0 + end) / 2;
	// 4 循环找出最大的放在堆顶
	for (int father = lastFather; father >= 0; father--)  // 它的下标减一的也都是父节点
	{
		// 2 根据父节点推算出左右孩子的下标
		int left = father * 2 + 1;
		int right = father * 2 + 2;
		// 3 在保证右孩子不越界的情况下,使用右孩子和父节点比较;或者直接左孩子与父亲比较
		if (right <= end && arr[right] > arr[father])	myswap(arr, right, father);
		if (arr[left] > arr[father])	myswap(arr, left, father);
	}
}

二、排序

由无序堆变成大根堆

交换堆顶和右下角叶子

数组长度减一

class Solution
{
public:
	void heap_sort(int arr[], int len)
	{
		for (int end = len - 1; end > 0;end--) {
			maxHeap(arr, end);
			myswap(arr, 0, end);
		}
		myprint(arr, len);
	}
};

时间复杂度 O(nlogn),空间复杂度 O(1),不稳定排序

9. 总结

时间复杂度、空间复杂度、稳定性

排序平均时间复杂度空间复杂度稳定性
选择selectionn^21不稳
冒泡bubblen^21
插入insertn^21
heapnlog_2n1不稳
希尔shelln^1.31不稳
归并mergenlog_2nn
快速quicknlog_2nlog_2n不稳
bucketn+kn+k
计数countingn+kn+k
基数radixn*kn+k

《打油诗》

选泡插

快归堆希桶计基

恩方恩老恩一三

对恩加k恩乘k

不稳稳稳不稳稳

不稳不稳稳稳稳

适用场景

(1)当数据规模较小时候,可以使用简单的直接插入排序或者直接选择排序

(2)当文件的初态已经基本有序,可以用直接插入排序和冒泡排序

(3)当数据规模较大时,可以考虑使用快速排序,要求排序时是稳定的,可以考虑用归并排序

(4)数据明显有几个关键字或者几个属性组成,用基数排序

(5)数据量大,但是取值范围小的场景,用计数排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值