第十三节:排序算法详解1(认识排序、冒泡、选择、插入排序)

一. 认识排序算法

1. 排序算法的定义

   在计算机科学与数学中,一个排序算法(英语:Sorting algorithm)是一种能将一串资料依照特定排序方式排列的算法。

2. 排序算法的分类标准

 计算的时间复杂度:使用大O表示法,也可以实际测试消耗的时间;

 内存使用量(甚至是其他电脑资源):比如外部排序,使用磁盘来存储排序的数据;

 稳定性:稳定排序算法会让原本有相等键值的纪录维持相对次序;

 排序的方法:插入、交换、选择、合并等等;

3. 常见的排序算法

 冒泡排序、 选择排序、 插入排序     【简单,易理解】

 归并排序、快速排序                        【非常重要,面试必考】

 堆排序、希尔排序                            【了解即可】

 计数排序、 桶排序、 基数排序、 内省排序、 平滑排序  【了解即可】

4. 时间复杂度

5. 后续的学习思路

 定义

 结合图片分析流程

 代码实操和代码优化

 时间复杂度分析

 总结

 

二. 冒泡排序

1. 定义

 基本思路是通过两两比较相邻的元素并交换它们的位置,从而使整个序列按照顺序排列。

 该算法一趟排序后最大值总是会移到数组最后面,那么接下来就不用再考虑这个最大值。

一直重复这样的操作,最终就可以得到排序完成的数组。

 这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数组的尾端,故名 “冒泡排序”

2. 流程

【默认说的都是由小到大排序】

   从第一个元素开始,逐一比较相邻元素的大小。

   如果前一个元素比后一个元素大,则交换位置。

   在第一轮比较结束后,最大的元素被移动到了最后一个位置。

   在下一轮比较中,不再考虑最后一个位置的元素,重复上述操作。

   每轮比较结束后,需要排序的元素数量减一,直到没有需要排序的元素。

   排序结束。

   这个流程会一直循环,直到所有元素都有序排列为止。

3. 推导过程

(1). 假设数组的长度length=4, 需要对其进行排序

(2). 显而易见, 需要进行3轮排序,即length-1轮

(3). 第1轮需要两两比较3次(length-1), 第2轮两两比较2次(length-1-1),第3轮两两比较1次(length-1-2),即length-1-i (i依次0,1,2)

(4). 以第1轮为例,为什么是length,而不是length-1?(这里指的是内层循环)

     因为:arr[j+1]中的j+1最大只能是3,如果这里用length就越界了,所以此处是length-1

(5).上述进行三轮的代码可以总结为两个for循环,最外层决定需要进行几轮排序,内层决定两两比较几次和交换

(6). 补充:外层循环写成i<length 是否可以呢?

         答:是可以的。还是以length=4为例,显而易见进行3轮排序即可, 虽然这里用的i<length,但是当i=3的时候,内层 j=length-1-i=0, 根本不走内层循环了

         所以还是进行了3轮排序。

/**
 * 01-冒泡的推导过程
 * @param arr 待排序的数组
 * @returns 返回从小到大排序后的数组
 */
function BundleSort1(arr: number[]): number[] {
	let length = arr.length; //测试的长度为4,只需要进行3轮比较即可

	//第1轮循环比较(arr[j+1]中的j+1最大只能是3,否则越界,所以此处是length-1)
	for (let j = 0; j < length - 1; j++) {
		if (arr[j] > arr[j + 1]) {
			//交换
			let temp = arr[j + 1];
			arr[j + 1] = arr[j];
			arr[j] = temp;
		}
	}
	//第2轮循环比较
	for (let j = 0; j < length - 1 - 1; j++) {
		if (arr[j] > arr[j + 1]) {
			//交换
			let temp = arr[j + 1];
			arr[j + 1] = arr[j];
			arr[j] = temp;
		}
	}
	//第3轮循环比较
	for (let j = 0; j < length - 1 - 2; j++) {
		if (arr[j] > arr[j + 1]) {
			//交换
			let temp = arr[j + 1];
			arr[j + 1] = arr[j];
			arr[j] = temp;
		}
	}
	return arr;
}

/**
 * 02-冒泡排序,合并推导
 * @param arr 待排序的数组
 * @returns 返回从小到大排序后的数组
 */
function BundleSort2(arr: number[]): number[] {
	let length = arr.length; // 测试的长度为4,只需要进行3轮比较即可
	//最外层:决定需要进行几轮比较
	for (let i = 0; i < length - 1; i++) {
		//内层:决定需要进行哪些两两大小比较
		for (let j = 0; j < length - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				//交换
				let temp = arr[j + 1];
				arr[j + 1] = arr[j];
				arr[j] = temp;
			}
		}
	}
	return arr;
}

 

4. 代码优化

(1). 交换方法抽离

   将交换方法抽离到myUtils中进行封装,这里有ES6的新写法

/**
 * 03-冒泡排序
 * 优化1:抽离交换方法
 * @param arr 待排序的数组
 * @returns 返回从小到大排序后的数组
 */
function BundleSort3(arr: number[]): number[] {
	let length = arr.length; // 测试的长度为4,只需要进行3轮比较即可
	//最外层:决定需要进行几轮比较
	for (let i = 0; i < length - 1; i++) {
		//内层:决定需要进行哪些两两大小比较
		for (let j = 0; j < length - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				swap(arr, j, j + 1); //交换
			}
		}
	}
	return arr;
}
/**
 * 01-交换索引i和j的位置
 * @param arr 目标数组
 * @param i 索引i
 * @param j 索引j
 */
function swap(arr: number[], i: number, j: number) {
	// let temp = arr[i];
	// arr[i] = arr[j];
	// arr[j] = temp;

	//ES6的新写法
	[arr[i], arr[j]] = [arr[j], arr[i]];
}

(2).优化循环次数

   如果再第二层循环中没有进行交换操作,就说明现在已经排序好了,直接跳出外层循环即可

/**
 * 04-冒泡排序
 * 优化2:减少最外层循环的轮数
 * @param arr 待排序的数组
 * @returns 返回从小到大排序后的数组
 */
function BundleSort4(arr: number[]): number[] {
	let length = arr.length; // 测试的长度为4,只需要进行3轮比较即可
	//最外层:决定需要进行几轮比较
	for (let i = 0; i < length - 1; i++) {
		let isSwap = false;
		//内层:决定需要进行哪些两两大小比较
		for (let j = 0; j < length - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				swap(arr, j, j + 1); //交换
				isSwap = true;
			}
		}
		//只要前一轮没有进行交换,就不需要进行了
		if (isSwap == false) {
			console.log(`常规需要进行${length - 1}轮,优化后${i}轮`);
			break; //退出最外层循环
		}
	}
	return arr;
}

5. 改为由小到大

   PS:直接一步到位,最佳写法, 实际上就是改为 arr[j] < arr[j+1]

/**
 * 04-冒泡排序
 * 改为:从大到小排序
 * @param arr 待排序的数组
 * @returns 返回从小到大排序后的数组
 */
function BundleSort5(arr: number[]): number[] {
	let length = arr.length; // 测试的长度为4,只需要进行3轮比较即可
	//最外层:决定需要进行几轮比较
	for (let i = 0; i < length - 1; i++) {
		let isSwap = false;
		//内层:决定需要进行哪些两两大小比较
		for (let j = 0; j < length - 1 - i; j++) {
			if (arr[j] < arr[j + 1]) {
				swap(arr, j, j + 1); //交换
				isSwap = true;
			}
		}
		//只要前一轮没有进行交换,就不需要进行了
		if (isSwap == false) {
			console.log(`常规需要进行${length - 1}轮,优化后${i}轮`);
			break; //退出最外层循环
		}
	}
	return arr;
}

6. 时间复杂度分析

(1). 最好 O(n)

   即待排序的序列已经是有序的。 此时仅需遍历一遍序列,不需要进行交换操作。

(2) .最坏 O(n^2)

    即待排序的序列是逆序的。需要进行n-1轮排序,每一轮中需要进行n-1-i次比较和交换操作。

(3).平均 O(n^2)

    即待排序的序列是随机排列的。每一对元素的比较和交换都有1/2的概率发生,因此需要进行n-1轮排序,每一轮中需要进行n-i-1次比较和交换操作。

总结:冒泡排序的时间复杂度主要取决于数据的初始顺序,最坏情况下时间复杂度是O(n^2),不适用于大规模数据的排序。

 

7. 补充测试工具封装

   封装testSort方法,测试准确行

  hy工具类中也有testSort方法、还有mesureSort方法测试性能

/**
 * 01-交换索引i和j的位置
 * @param arr 目标数组
 * @param i 索引i
 * @param j 索引j
 */
function swap(arr: number[], i: number, j: number) {
	// let temp = arr[i];
	// arr[i] = arr[j];
	// arr[j] = temp;

	//ES6的新写法
	[arr[i], arr[j]] = [arr[j], arr[i]];
}

/**
 * 02-校验该数组是否是从小到大排序
 * @param arr 待校验的数组
 * @returns ture or false
 */
function isToMaxSorted(arr: number[]): boolean {
	let length = arr.length;
	for (let i = 0; i < length - 1; i++) {
		if (arr[i] > arr[i + 1]) return false;
	}
	return true;
}

type SortAlgoFn = (arr: number[]) => number[];
function testSort(sortFn: SortAlgoFn) {
	//1.声明一个长度为10的数组
	let nums = Array.from({ length: 10 }, () => {
		return Math.floor(Math.random() * 200);
	});
	//2.对数组进行排序
	console.log('排序前的数组:', nums);
	const newNums = sortFn(nums);
	console.log('排序后的新数组:', nums);
	console.log('是否排序后有正确的顺序?', isToMaxSorted(newNums));
}

// 对外导出
export { swap, isToMaxSorted, testSort };

8. 总结

    冒泡排序适用于数据规模较小的情况,因为它的时间复杂度为O(n^2),对于大数据量的排序会变得很慢。

    ◼ 同时,它的实现简单,代码实现也容易理解,适用于学习排序算法的初学者。

    ◼ 但是,在实际的应用中,冒泡排序并不常用,因为它的效率较低。

    ◼ 因此,在实际应用中,冒泡排序通常被更高效的排序算法代替,如快速排序、归并排序等。

 

三. 选择排序

1. 定义

  首先在未排序的数列中找到最小元素,然后将其存放到数列的起始位置

  接着,再从剩余未排序的元素中继续寻找最小元素,然后放到已排序序列的末尾

  以此类推,直到所有元素均排序完毕。

2. 流程分析

 【默认由小到大排序】

  核心:内层的一轮循环比较下来,找到选择出来minIndex的位置,然后看是否需要交换

  A. 遍历数组,找到未排序部分的最小值

   (1) 首先,将未排序部分的第一个元素标记为最小值

   (2) 然后,从未排序部分的第二个元素开始遍历,依次和已知的最小值进行比较

   (3) 如果找到了比最小值更小的元素,就更新最小值的位置

  B. 将未排序部分的最小值放置到已排序部分的后面

   (1) 首先,用解构赋值的方式交换最小值和已排序部分的末尾元素的位置(即调用swap方法)

   (2) 然后,已排序部分的长度加一,未排序部分的长度减一

  C. 重复执行步骤 1 和 2,直到所有元素都有序

 

3. 代码实操

/**
 * 02-选择排序
 * 优化1-内层一轮,最多只交换1次
 * 10万数据 selectionSort1 20s  selectionSort2 7s
 * @param arr 待排序的数组
 * @returns 从小到大排序的数据
 */
function selectionSort2(arr: number[]): number[] {
	let length = arr.length;
	//假设长度length为4  [5,3,7,1], 最外层循环为3次, 即length-1, 剩下的最后一个一定是最大的
	for (let i = 0; i < length - 1; i++) {
		let minIndex = i;
		//内层:决定从哪个位置开始选择,直到渠道最后一个元素结束
		for (let j = i + 1; j < length; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j;
			}
		}
		//判断比较
		if (minIndex !== i) {
			swap(arr, i, minIndex);
		}
	}
	return arr;
}

4. 时间复杂度

 (1).最好情况时间复杂度:O(n^2)

    最好情况是指待排序的数组本身就是有序的。

    在这种情况下,内层循环每次都需要比较 n-1 次,因此比较次数为 n(n-1)/2,交换次数为 0。 所以,选择排序的时间复杂度为 O(n^2)。

 (2).最坏情况时间复杂度:O(n^2)

   最坏情况是指待排序的数组是倒序排列的。

   在这种情况下,每次内层循环都需要比较 n-i-1 次,因此比较次数为 n(n-1)/2,交换次数也为 n(n-1)/2。所以,选择排序的时间复杂度为 O(n^2)。

 (3).平均情况时间复杂度:O(n^2)

   平均情况是指待排序的数组是随机排列的。

   在这种情况下,每个元素在内层循环中的位置是等概率的,因此比较次数和交换次数的期望值都是 n(n-1)/4。所以,选择排序的时间复杂度为 O(n^2)。

5. 总结

  虽然选择排序的实现非常简单,但是它的时间复杂度较高,对于大规模的数据排序效率较低。

  如果需要对大规模的数据进行排序,通常会选择其他更为高效的排序算法,例如快速排序、归并排序等。

  总的来说,选择排序适用于小规模数据的排序和排序算法的入门学习,对于需要高效排序的场合,可以选择其他更为高效的排序算法。

 

四. 插入排序

1. 定义

 (1).首先假设第一个数据是已经排好序的,接着取出下一个数据,在已经排好序的数据从后往前扫描找到比它小的数的位置,将该位置之后的数整体后移一个单位,然后再将该数插入到该位置

 (2).不断重复上述操作,直到所有的数据都插入到已经排好序的数据中,排序完成。

2. 流程分析

    ① 首先,假设数组的第一个元素已经排好序了,因为它只有一个元素,所以可以认为是有序的。

    ② 然后,从第二个元素开始,不断与前面的有序数组元素进行比较。

    ③ 找到比它小的数的位置,将该位置之后的数整体后移一个单位。然后再将该数插入到该位置。

    ④ 否则,继续与前面的有序数组元素进行比较。

    ⑤ 以此类推,直到整个数组都有序。

    ⑥ 循环步骤2~5,

3. 代码实操

最后为什么是 arr[j + 1] = insertItem;  

可以从这个角度理解,假设insetItem比arr[0]还小,此时跳出while后j=-1,肯定是j+1=0 来赋值了

function insertionSort(arr: number[]): number[] {
	let length = arr.length;
	//第一层:表示从第i个元素,(表示需要进行几轮比较)
	for (let i = 1; i < length; i++) {
		//第二层:依次与前面有序数组进行比较
		let j = i - 1;
		let insertItem = arr[i];
		while (arr[j] > insertItem && j >= 0) {
			arr[j + 1] = arr[j];
			j--;
		}
		arr[j + 1] = insertItem;
	}
	return arr;
}

4. 时间复杂度

(1) 最好情况: O(n)

     如果待排序数组已经排好序

     那么每个元素只需要比较一次就可以确定它的位置,因此比较的次数为 n-1,移动的次数为 0。

     所以最好情况下,插入排序的时间复杂度为线性级别,即 O(n)。

(2) 最坏情况: O(n^2)

     如果待排序数组是倒序排列的

     那么每个元素都需要比较和移动 i 次,其中 i 是元素在数组中的位置。

     因此比较的次数为 n(n-1)/2,移动的次数也为 n(n-1)/2。

     所以最坏情况下,插入排序的时间复杂度为平方级别,即 O(n^2)。

(3) 平均情况  O(n^2):

     对于一个随机排列的数组,插入排序的时间复杂度也为平方级别,即 O(n^2)。

 

5. 总结

  (1) 如果数组部分有序,插入排序可以比冒泡排序和选择排序更快。但是如果数组完全逆序,则插入排序的时间复杂度比较高,不如快速排序或归并排序。

  (2) 插入排序虽然没有快速排序和归并排序等高级排序算法的复杂性和高效性,但是它的实现非常简单,而且在一些特定的场景下表现也很好。

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2024-01-17 08:26  Yaopengfei  阅读(18)  评论(1编辑  收藏  举报