总结一下排序算法,还是需要多加练习,弄不清楚就画图体会。附上一张帅图
排序
任何一种排序算法都没有优劣之分 排序不是比较大小,排序的本质是比较和交换
冒泡排序
// 冒泡排序,一圈只能排出一个数
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];
function compare(a, b) {
// 判断是否需要交换
return a > b;
}
function exchange(arr, a, b) {
// 将数组中的a,b位置的值交换
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
function sort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
//可能会问为什么i < arr.length - 1,因为你要保证arr[i + 1]有值
if (compare(arr[i], arr[i + 1])) {
exchange(arr, i, i + 1);
}
}
}
sort(arr);
console.log(arr);
/*
[
4, 3, 1, 2, 5,
8, 7, 10, 9, 12
]
*/
完整的冒泡排序
// 冒泡排序
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];
function compare(a, b) {
// 判断是否需要交换
return a > b;
}
function exchange(arr, a, b) {
// 将数组中的a,b位置的值交换
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
function sort(arr) {
for (var j = 0; j < arr.length; j++) {
// 第几圈就少几次,因为后面的已定
for (var i = 0; i < arr.length - 1 - j; i++) {
if (compare(arr[i], arr[i + 1])) {
exchange(arr, i, i + 1);
}
}
}
}
sort(arr);
console.log(arr);
/*
[Inversion
1, 2, 3, 4, 5,
7, 8, 9, 10, 12
]
*/
插入排序
一般也被称为直接插入排序Insert Sort。对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增 1 的有序表。在其实现过程使用双层循。
外层循环对除了第一个元素之外的所有元素遍历 内层循环对当前元素前面有序表进行待插入位置查找,并进行移动
let originArr = [2, 3, 10, 8, 5, 2];
function insertSort(arr) {
for (var i = 1; i < arr.length; i++) {
for (var j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
insertSort(originArr);
console.log(originArr);
二分查找(重要)
二分查找的条件不是必须为有序排列(刻板印象上二分一定有序,但是不是的,关键是需要找到能够二分的那个点)
主要就是要注意这个数组取值的范围,就比如我们取 end 赋值为 arr.length 和 arr.length-1 的效果就有很多要注意的地方
let serachArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
function getNumIndex(arr, target) {
let begin = 0;
let end = arr.length; //注意这个end是取不到的,
while (
begin < end //这个end的边界情况是大于begin的,没有begin==end的情况
// 如果end==arr.length-1那么这里的条件就是begin <= end因为相等时还有一个数,随之,下面更新边界情况也发生变化
) {
let midIndex = Math.floor((end + begin) / 2); //(begin + end) >>> 1;
if (arr[midIndex] == target) {
return midIndex;
} else if (arr[midIndex] > target) {
end = midIndex;
} else if (arr[midIndex] < target) {
begin = midIndex + 1;
}
}
return -1;
}
console.log(getNumIndex(serachArr, 8));
// 递归写法
function getNumIndex(arr, target) {
function getIndex(arr, target, begin, end) {
if (begin >= end) return -1;
let midIndex = Math.floor((end + begin) / 2);
if (arr[midIndex] == target) {
return midIndex;
} else if (arr[midIndex] > target) {
return getIndex(arr, target, begin, midIndex);
} else {
return getIndex(arr, target, midIndex + 1, end);
}
}
return getIndex(arr, target, 0, arr.length);
}
console.log(getNumIndex(serachArr, 8));
选择排序
选择排序的内层循环,每一圈选择一个最大的放到还没有排序的数的最后面
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];
function compare(a, b) {
// 判断是否需要交换
return a < b;
}
function exchange(arr, a, b) {
// 将数组中的a,b位置的值交换
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 选择排序
function selectSort(arr) {
for (var i = 0; i < arr.length; i++) {
var maxIndex = 0; //假设第一个数是最大值
for (var j = 0; j < arr.length - i; j++) {
if (compare(arr[maxIndex], arr[j])) {
// 记录最大值
maxIndex = j;
}
}
// 一次循环过后交换
exchange(arr, maxIndex, arr.length - 1 - i);
}
}
selectSort(arr);
console.log(arr);
归并排序(分治思想)
把无序的数组细分为小数组,我们让小数组有序,然后把小数组在合并到一起。
// 归并排序
let arr = [2, 3, 4, 2, 34, 3, 2, 1, 78, 4];
// 先让子序列有序然后把子序列合并,就是整体的有序序列
function mergeSort(arr, l, r) {
if (l == r) {
return;
}
// 找到中点,然后左边分治、右边分治
let mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, r, mid); // 在这个区间上有序,注意要有中间值做整合的
function merge(arr, begin, end, mid) {
let newArr = new Array(end - begin + 1); //开拓二外的子数组空间
let index = 0; //子数组指针
let i = begin; //左分治指针
let j = mid + 1; //右分治指针
// 以下是左右分治指针均未越界
while (i <= mid && j <= end) {
newArr[index++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
// 以下两种情况实际只会跑一个
while (i <= mid) {
// 左指针未越界
newArr[index++] = arr[i++];
}
while (j <= end) {
// 右指针未越界
newArr[index++] = arr[j++];
}
// 这个时候我们的原数组还没有改变,一直在操作额外开辟的数组空间
for (var n = 0; n < newArr.length; n++) {
arr[begin + n] = newArr[n]; //把这个小数组的分治结果添加到原数组
}
}
}
mergeSort(arr, 0, arr.length - 1);
console.log(arr);
求小和问题
let arr = [1, 3, 4, 2, 5];
// 求最小和
function toSum(arr, begin, end) {
if (begin >= end) {
return 0;
}
let mid = (end + begin) >> 1; //找中点,分治
return (
toSum(arr, begin, mid) +
toSum(arr, mid + 1, end) +
merge(arr, begin, end, mid)
); //合并
function merge(arr, begin, end, mid) {
let helpArr = new Array(end - begin + 1); //开辟额外的空间
let l = begin; //左指针
let r = mid + 1; //右指针
let res = 0; //和
let i = 0; //指针
while (l <= mid && r <= end) {
// 两个指针都没有越界
res += arr[l] < arr[r] ? end - r + 1 * arr[l] : 0;
helpArr[i++] = arr[l] < arr[r] ? arr[l++] : arr[r++];
}
while (l <= mid) {
helpArr[i++] = arr[l++];
}
while (r <= end) {
helpArr[i++] = arr[r++];
}
for (let i = 0; i < helpArr.length; i++) {
arr[begin + i] = helpArr[i];
}
return res;
}
}
console.log(toSum(arr, 0, arr.length - 1));
leetcode 315 51(都是归并排序的变式)
简单快速排序
思想就是,创建left和right两个数组,分别以leader为基准,分来,然后再分开
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];
function quickSort(arr) {
if (arr == null || arr.length == 0) return [];
// 先选择一个基准
var leader = arr[0];
// 小的站左边,大的站右边
var left = [];
var right = [];
for (var i = 1; i < arr.length; i++) {
if (arr[i] < leader) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
// 递归
left = quickSort(left);
right = quickSort(right);
// 拼接起来
left.push(leader);
return left.concat(right);
}
console.log(quickSort(arr));
标准快速排序
标准的快速排序不会创建数组,只会在原数组上进行操作.(比简单快速排序难理解)
- 定义一个
leader - 做两个指针
left和right
我们希望左侧的都比
leader小,右侧的都比leader大left从左开始知道找到比leader大的数(min)right从右开始知道找到比leader小的数(max) 随后交换 min 和 max
第一遍交换
仍然是完成第一圈;
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];
function exchange(arr, a, b) {
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
function quickSort(arr, begin, end) {
if (begin >= end - 1) {
return;
}
// 定义左右指针
var left = begin;
var right = end;
do {
// 这里面的leader默认就是arr[begin]
do {
left++; //左指针往右走
} while (left < right && arr[left] < arr[begin]);
do {
right--; //右指针像左走
} while (right > left && arr[right] > arr[begin]);
if (left < right) {
// 在完成上面两个do...while...之后,left指向第一个比leader大的数,right指向第一个比leader小的数
exchange(arr, left, right);
}
} while (left < right);
// leader要交换的点
var centerIndex = left == right ? right - 1 : right;
exchange(arr, begin, centerIndex);
}
// 标准快排
function balanceQuickSort(arr) {
quickSort(arr, 0, arr.length); //传入需要排序的范围,在数组中arr.length取不到
}
balanceQuickSort(arr);
console.log(arr);
完成的标准快排
就是说再递归左边和右边
var arr = [4, 12, 3, 1, 2, 5, 8, 7, 10, 9];
function exchange(arr, a, b) {
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
function quickSort(arr, begin, end) {
if (begin >= end - 1) {
return;
}
// 定义左右指针
var left = begin;
var right = end;
do {
// 这里面的leader默认就是arr[begin]
do {
left++; //左指针往右走
} while (left < right && arr[left] < arr[begin]);
do {
right--; //右指针像左走
} while (right > left && arr[right] > arr[begin]);
if (left < right) {
// 在完成上面两个do...while...之后,left指向第一个比leader大的数,right指向第一个比leader小的数
exchange(arr, left, right);
}
} while (left < right);
// leader要交换的点
var centerIndex = left == right ? right - 1 : right;
exchange(arr, begin, centerIndex);
// 对左边再排(递归)
quickSort(arr, begin, centerIndex);
// 对右边再排(递归)
quickSort(arr, centerIndex + 1, end);
}
// 标准快排
function balanceQuickSort(arr) {
quickSort(arr, 0, arr.length); //传入需要排序的范围,在数组中arr.length取不到
}
balanceQuickSort(arr);
console.log(arr);
不创建额外空间对数组按照某值分为两类(双指针)
不创建额外空间,将数组分类
/**
*
* @param {*} arr 无序数组
* @param {*} border 界定值
*/
// 不创建额外的空间,将数组按照边界值成两组
function pointerBorder(arr, border) {
let p1 = 0; //左指针,控制边界
let p2 = 0; //右指针,控制遍历
while (p2 <= arr.length - 1) {
if (arr[p2] <= border) {
var temp = arr[p2];
arr[p2++] = arr[p1];
arr[p1++] = temp;
} else {
p2++;
}
}
}
pointerBorder(arr, 8);
荷兰国旗问题
就如荷兰国旗所示:我们需要将一个无序的数组,按照给定的某一个界定值target将数组分成三部分就好比荷兰的三色旗,分为小于、等于、大于三部分
let arr = [2, 3, 6, 8, 6, 7];
/**
*
* @param {*} arr 数组
* @param {*} target 界定值
*/
function flagColorQues(arr, target) {
let p1 = 0; //左区域指针
let p2 = arr.length - 1; //右区域指针
let i = 0; //遍历指针
while (i <= p2 && p1 <= p2 && p1 <= i) {
// 这个while循环条件也是要注意,当他们相等的情况下是有一个元素的,如果条件错误写出的结果也是错的
if (arr[i] == target) {
i++;
} else if (arr[i] < target) {
var temp = arr[i];
arr[i++] = arr[p1];
arr[p1++] = temp;
} else if (arr[i] > target) {
// 注意这个遍历指针不用++
var temp = arr[i];
arr[i] = arr[p2];
arr[p2--] = temp;
}
}
}
flagColorQues(arr, 6);
console.log(arr);
堆排序
堆是一种特殊的二叉树结构也叫二叉堆,堆分为大根堆和小根堆两种,由于堆的特性能够高效、快速的找到最大,最小值,常被用于优先队列中,也被用于著名的堆排序算法
大根堆的特性(小根堆相反)
- 每个节点如果有孩子节点,那么它的的值大于或等于它的左、右孩子节点的值
- 如果用数组表示大根堆那么数组的第一项就是这个数组的最大值,
排序
- 首先是把原数组变成一个大根堆,那么这个数组的最大值我们就能够确定是数组的第一项
- 随后将数组的第一项和堆的最后一位交换位置,堆的长度--,那么这个最大值就不再这个堆的范围内,后续也不会对他再做处理,因为我们要处理的就是还在堆中的元素
- 在交换过后,在堆的范围内就不再是大根堆,这个时候就需要对新的
arr[0]位置的元素放到合适的位置重新构成大根堆 - 随后执行第二步第三步操作,直到堆里不再有数,那么我们每次操作都拿出了堆中的最大值并且放到了合适的地方
let arr = [12, 41, 42, 33, 45, 23, 7, 9, 10, 2]; //7
// 堆排序
function heapSort(arr) {
if (arr.length < 2 || arr == null) return null;
let heapSize = 0; //记录堆的深度,我们认为一开始没有堆
for (var i = 0; i < arr.length; i++) {
heapInsert(arr, i); //创建堆的过程
heapSize++; //7
}
while (heapSize > 0) {
// 堆里还有值,就需要让这个堆有序
exchange(arr, 0, --heapSize); //交换最后一位
heapLify(arr, 0, heapSize - 1); //创建的新的大根堆,不包括最后一位,影响下面left的循环条件包括右孩子
}
// 创建大根堆,向上
function heapInsert(arr, index) {
while (arr[index] > arr[Math.floor((index - 1) / 2)]) {
exchange(arr, index, Math.floor((index - 1) / 2));
index = Math.floor((index - 1) / 2);
}
}
// 向下堆化
function heapLify(arr, index, heapSize) {
let left = index * 2 + 1;
while (left <= heapSize) {
//说明左孩子还在这个堆的范围内,
// 这个index的下标的数需要更新,和他的两个孩子中大的那个交换
// 有右孩子就需要比较,否则就是左孩子,这个条件我最开始就没有考虑完全就会出错
let lagest =
left + 1 <= heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
lagest = arr[index] > arr[lagest] ? index : lagest; //子节点比较完了和父节点比较,因为子节点的最大值不一定比他大
if (lagest == index) {
// 这种就不用交换
break;
}
exchange(arr, index, lagest); //下标不一样的才交换
index = lagest; //下标更新
left = index * 2 + 1; //下一个循环
}
}
// 交换
function exchange(arr, a, b) {
var temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
heapSort(arr);
console.log(arr);