数据结构与算法面试要点简明教程(十)—— 排序

参考:https://blog.csdn.net/jiaoyangwm/article/details/80808235

https://blog.csdn.net/a2392008643/article/details/81781766

https://mp.weixin.qq.com/s/vn3KiV-ez79FmbZ36SX9lg

本文仅是将他人博客经个人理解转化为简明的知识点,供各位博友快速理解记忆,并非纯原创博客,如需了解详细知识点,请查看参考的各个原创博客。

目录

第十章  排序

10.1  排序算法综述

10.2  冒泡排序

10.3  选择排序

10.4  插入排序

10.5  希尔排序

10.6  归并排序

10.7  快速排序

10.8  堆排序

10.9  计数排序

10.10  桶排序

10.11  基数排序

10.12  相关面试题


第十章  排序

10.1  排序算法综述

内排序与外排序:

根据排序过程中,待排序记录是否全部被放置在内存中,排序分为:内排序和外排序

  • 内排序:排序过程中,待排序的所有记录全部被放置在内存中(主要介绍内排序)
  • 外排序:排序过程中,待排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次进行数据交换才可以。

排序的稳定性:

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

10.2  冒泡排序

算法步骤:

  1. 从前往后比较相邻的两个元素,如果第一个比第二个大,就交换他们两个,进行length-1轮;
  2. 上一步做完i轮后,最后的i元素会是有序且最大的数。针对除最后i个外的所有的元素重复以上的步骤直到没有任何一对数字需要比较。

算法代码:

void BubbleSort(int* arr, int length) {
	//外层是比较次数,总共要经过 N-1 轮比较
	for (int i = 1; i < length; i++) {
		//设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成
		bool flag = true;
		for (int j = 0; j < length - i; j++) {
			if (arr[j] > arr[j + 1]) {
				swap(arr[j], arr[j + 1]);
				flag = false;
			}
		}
		if (flag) break;
	}
}

10.3  选择排序

算法步骤:

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
  3. 重复第二步,直到所有元素均排序完毕。

算法代码:

void SelectionSort(int* arr, int length) {
	for (int i = 0; i < length-1; i++) {
		// 记录目前能找到的最小值元素的下标
		int min = i;
		// 每轮需要从i后第一个数字开始比到末尾
		for (int j = i+1; j < length; j++) {
			if (arr[j] < arr[min]) min = j;
		}
		// 将找到的最小值和i位置所在的值进行交换
		if (i != min) swap(arr[i], arr[min]);
	}
}

10.4  插入排序

算法步骤:

  1. 将第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列;
  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面)。

算法代码:

void InsertionSort(int* arr, int length) {
	for (int i = 1; i < length; i++) {
		// 记录要插入的数据
		int tmp = arr[i];
		// 从有序序列最右边的开始比较,找到比arr[i]小的数(比大的往右移动)
		int j = i - 1;
		while (j >= 0 && tmp < arr[j]) {
			arr[j + 1] = arr[j];
			j--;
		}
		arr[j + 1] = tmp;
	}
}

10.5  希尔排序

利用插入排序的思想,考虑到插入排序在序列基本有序且数量较少时性能较高,因此先对序列进行逻辑上的分组然后再进行插入排序。

算法步骤:

  1. 设定初始增量t;
  2. 每趟排序,根据对应的增量 t,将待排序列分割成若干长度为 m 的子序列,分别对各子序列进行直接插入排序;
  3. 随后减少增量,增加分组,继续对每个子序列进行插入排序,直到增量为1,整个序列作为一个子序列来处理,子序列长度即为整个序列的长度。

算法代码:

void ShellSort(int* arr, int length) {
	for (int gap = length / 2; gap > 0; gap /= 2) {
		for (int i = gap; i < length; i++) {
			// 记录要插入的数据
			int tmp = arr[i];
			// 从有序子序列最右边的开始比较,找到比arr[i-gap]小的数(比大的往右移动)
			int j = i - gap;
			while (j >=0 && tmp < arr[j]) {
				arr[j + gap] = arr[j];
				j -= gap;
			}
			arr[j + gap] = tmp;
		}
	}
}

10.6  归并排序

使用分治思想,将原始序列分为两部分分别排序,然后合并,重点在于合并(治)过程。

算法步骤:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动该指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。(这些都是治的过程)

算法代码:

void merge(int* arr, int L, int M, int R) {
	//申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
	int* tmp = new int[R - L + 1];
	int i = 0;
	//设定两个指针,最初位置分别为两个已经排序序列的起始位置
	int pFirst = L;
	int pRight = M + 1;
	//比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动该指针到下一位置,重复该步骤直到某一指针达到序列尾
	while (pFirst <= M && pRight <= R) {
		tmp[i++] = arr[pFirst] < arr[pRight] ? arr[pFirst++] : arr[pRight++];
	}
	//将另一序列剩下的所有元素直接复制到合并序列尾
	while (pFirst <= M) {
		tmp[i++] = arr[pFirst++];
	}
	while (pRight <= R) {
		tmp[i++] = arr[pRight++];
	}
	//将临时数组的数据存入原数组
	for (int j = 0; j < (R - L + 1); j++) {
		arr[L + j] = tmp[j];
	}
}
void MergeSort(int* arr, int L, int R) {
	if (L == R) {
		return;
	}
	int mid = (L + R) / 2;
	MergeSort(arr, L, mid);
	MergeSort(arr, mid + 1, R);
	merge(arr, L, mid, R);
}
void MergeSort(int* arr, int length) {
	if (arr == nullptr || length<2) {
		return;
	}
	MergeSort(arr, 0, length - 1);
}

10.7  快速排序

与归并排序类似,也使用分治思想。

算法步骤:

  1. 从数列中挑出一个元素,称为 “基准”(pivot),一般选择最后一个元素;
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边);
  3. 在这个分区退出之后,该基准就处于数列的中间位置,这个称为分区(partition)操作;
  4. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

算法代码:

int partition(int* arr, int L, int R) {
	//记录基准的值
	int pivot = arr[L];
	while (L < R) {
		//从右往左找到小于基准的值的索引
		while (L < R && pivot <= arr[R]) R--;
		//将该值与左指针所指位置的值交换
		if (L < R) arr[L++] = arr[R];
		//从左往右找到大于基准的值
		while (L < R && pivot >= arr[L]) L++;
		//将该值与右指针所指位置的值交换
		if (L < R) arr[R--] = arr[L];
	}
	//将左右指针重叠位置(索引)替换为基准值,此时该指针左边全小于基准,右边全大于基准
	arr[L] = pivot;
	//返回分区位置索引
	return L;
}
void QuickSort(int* arr, int L, int R) {
	if (L >= R) return;
	int M = partition(arr, L, R);
	QuickSort(arr, L, M - 1);
	QuickSort(arr, M + 1, R);
}
void QuickSort(int* arr, int length) {
	if (arr == nullptr || length<2) {
		return;
	}
	QuickSort(arr, 0, length - 1);
}

10.8  堆排序

首先,我们列出堆的基本形式:

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] 

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] 

 

算法步骤:

  1. 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点;
  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  3. 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,继续交换堆顶元素与当前末尾元素;
  4. 反复执行调整+交换步骤,直到整个序列有序。

算法代码:

void heapify(int* arr, int i, int length) {
	//找到该节点左右两个子节点的索引
	int left = 2 * i + 1;
	int right = 2 * i + 2;
	//记录值最大节点的索引
	int largest = i;
	//如果左子节点大于该节点,则将值最大索引改为左子节点索引
	if (left < length && arr[left] > arr[largest]) {
		largest = left;
	}
	//如果右子节点大于该节点,则将值最大索引改为右子节点索引
	if (right < length && arr[right] > arr[largest]) {
		largest = right;
	}
	//如果值最大索引被改动过,则替换两个值,并对替换后的子节点也进行最大堆调整
	if (largest != i) {
		swap(arr[largest], arr[i]);
		heapify(arr, largest, length);
	}
}
void HeapSort(int* arr, int length) {
	//1.构建最大堆
	for (int i = length / 2 - 1; i >= 0; i--) {
		//从第一个非叶子结点开始,从下至上,从右至左调整结构
		heapify(arr, i, length);
	}
	//2.交换堆顶元素与末尾元素+调整剩余堆结构
	for (int j = length - 1; j > 0; j--) {
		swap(arr[0], arr[j]);
		heapify(arr, 0, j);
	}
}

10.9  计数排序

算法步骤:

  1. 花O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max;
  2. 开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1) ;
  3. 数组 B 中 index 的元素记录的值是 A 中某元素出现的次数;
  4. 最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素以及对应的个数。

算法代码:

void CountSort(int* arr, int length) {
	int max = arr[0];
	int lastIdx = 0;
	//1.找到数组中的最大值
	for (int i = 1; i < length; i++) {
		max = arr[i] > max ? arr[i] : max;
	}
	//2.开辟一块空间,将数组中数字出现次数记录入空间内
	int* sortArr = new int[max + 1]();
	for (int j = 0; j < length; j++) {
		sortArr[arr[j]]++;
	}
	//3.把空间内的值输入到原数组中
	for (int k = 0; k < max + 1; k++) {
		while (sortArr[k] > 0) {
			arr[lastIdx++] = k;
			sortArr[k]--;
		}
	}
}

10.10  桶排序

桶排序是: 桶思想排序 + 一个普通的排序(常用快速排序)。

算法步骤:

  1. 找到数组中最大最小值,以此获得桶的个数((maxValue - minValue) / bucketSize + 1);
  2. 将数组数据映射入相应桶中;
  3. 对每个桶进行排序,并将排序后的数据插入原数组中。

算法代码:

void BucketSort(int* arr, int length, int bucketSize) {
	if (length < 2) return;
	// 找到数组中最大最小值
	int minValue = arr[0], maxValue = arr[0];
	for (int i = 0; i < length; i++) {
		minValue = arr[i] < minValue ? arr[i] : minValue;
		maxValue = arr[i] > maxValue ? arr[i] : maxValue;
	}
	//得到桶的个数
	int bucketCnt = (maxValue - minValue) / bucketSize + 1;
	vector<vector<int>> buckets(bucketCnt);
	// 利用映射函数将数据分配到各个桶中
	for (int j = 0; j < length; j++) {
		int index = (arr[j] - minValue) / bucketSize;
		buckets[index].push_back(arr[j]);
	}
	int arrIdx = 0;
	for (auto bucket : buckets) {
		if (bucket.size() <= 0) continue;
		//对每个桶进行排序(此处采用插入排序)
		int* tmpArr = new int[bucket.size()]();
		copy(bucket.begin(), bucket.end(), tmpArr);
		InsertionSort(tmpArr, bucket.size());
		//将桶内元素写入原数组
		for (int t = 0; t < bucket.size(); t++) {
			cout << tmpArr[t] << endl;
			arr[arrIdx++] = tmpArr[t];
		}
	}
}

10.11  基数排序

桶排序是: 桶思想排序 + 一个普通的排序(常用快速排序)。

算法步骤:

  1. 取得数组中的最大数,并取得位数,将所有待比较数值(正整数)统一为同样的位数长度,位数较短的在前面补零;
  2. 从最低位开始,依次进行计数排序;
  3. 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

算法代码:

//拿到传入数的位数
int getRadixCount(int count) {
	int num = 1;
	if (count / 10 >0) {
		num++;
	}
	return num;
}
void RadixSort(int* arr, int length) {
	int max = arr[0];
	for (int i = 1; i< length; i++) {
		max = arr[i]>max ? arr[i] : max;
	}
	int radixCount = getRadixCount(max);
	int mod = 10, dev = 1;
	//遍历和最大值位数相等的次数(计数排序)
	for (int i = 0; i < radixCount; i++, mod *= 10, dev *= 10) {
		// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
		vector<vector<int>> counter(mod * 2);
		for (int j = 0; j < length; j++) {
			int bucket = ((arr[j] % mod) / dev) + mod;
			counter[bucket].push_back(arr[j]);
		}
		int pos = 0;
		for (auto bucket : counter) {
			for (int value : bucket) {
				arr[pos++] = value;
			}
		}
	}
}

10.12  相关面试题

Q:

A:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值