排序算法是所以很多知识的一个体现,也能很好的培养我们的算法思维,小白本人大学期间这些算法都曾将在数据结构这门课程中学习过,这里再带大家一起温习一下。
文章目录
工具
一、比较算法
顾名思义,就是通过与基数或者相邻之间进行比较,来实现排序的目的。
1.1交换排序
交换排序是在比较的过程中比较的两个元素在比较之后交换位置,这里有:
- 冒泡排序
- 定向冒泡
- 快排
1.1.1冒泡排序
原理是依次比较两个相邻的元素,如果他们的顺序错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成。因为这个原理像冒泡一样把元素一点点的“冒”出来,所以起名叫冒泡排序。
待排数据:34,12,56,12,46,112,8,6,17,33
bubbleSort:function () {
let a = [34,12,56,12,46,112,8,6,17,33];
console.log("这是初始数据:"+a+"\n");
for (let i = 0;i<a.length;i++){
for (let j=i+1;j<a.length;j++){
if(a[i]>a[j]){
[a[i],a[j]]=[a[j],a[i]];//解构复制,用起来很方便
}
}
console.log("这是第"+(i+1)+"次排序:"+a+"\n");
}
}
运行结果如下:
可以看到像冒泡一样,数据一个个冒了上来
时间复杂度是O(n2)
空间复杂度O(1)(解构复制也需要临时空间存储)
1.1.2定向冒泡排序(鸡尾酒排序)
定向冒泡本质还是冒泡排序,他只是将冒泡排序的的一冲循环内做了更多的事情,比如进行两次冒泡,一次想左找出最小,一次向右找出最大(默认是升序排序),是对冒泡排序的一种优化。
doubleBubbleSort:function () {
let a = [34,12,56,12,46,112,8,6,17,33];
console.log("这是初始数据:"+a+"\n");
for (let left = 0,right = a.length-1,n=1;left<=right;left++,right--,n++){
for (let i=left;i<=right;i++){//第一次向左冒泡
if (a[left]>a[i]){
[a[left],a[i]]=[a[i],a[left]];//解构复制
}
}
for (let j=right;j>=left;j--){//第二次向右冒泡
if (a[right]<a[j]){
[a[right],a[j]]=[a[j],a[right]];//解构复制
}
}
console.log("这是第"+(n)+"次排序:"+a+"\n");
}
}
这个排序算法的时间负责度为O(n2)
空间负责度为O(1)O
1.1.3快速排序
快排是平时使用频率最高的一种算法,真正执行发现确实是高效,运用了二分思想和递归来实现,当然了实现的方式很多,下面是我实现的一种。
其每次排序的过程是,左右开始向中间移动,不断的用左右两边的数据和初始数据进行比较,直至左边遇到大于基准,右边遇到小于基准则停止,交换位置,直到两边相遇:
4. 选择基准,我们以比较区域的第一个元素(也可以其他的,这是我的这个算法我自己指定的)作为比较的基准数据(第一次默认是a[0],注意理解“比较区域”)
5. 向右移i,直到遇到第一个大于m的时候停下来
6. 向左移j,直到遇到第一个小于m的时候停下来
7. 交换元素
8. 知道i==j停止,第一次循环结束
9. 之后进行m分解左右执行递归(注意边界,递归区域保证最少有两个元素,左右的边界是上次终止的+1和-1)
num = 0;
function quickSort (left,right,a,m) {
//看下面调用,第一次m是a[0],之后均是比较区域的第一个
var i=left,j=right;
while(i<j&&num<100){
while(a[i]<m&&i<j){//向左移i,直到遇到第一个大于m的时候停下来
i++;
}
while (a[j]>=m&&i<j){ // 向左移j,直到遇到第一个小于m的时候停下来
j--;
}
[a[i],a[j]] = [a[j],a[i]];
this.num++;
console.log("这是第"+num+"次排序:"+a+"\n");
}
if(a[i]<m){
if(j==right){
i--;
}else{
j++;
}
}else{
if(i==left){
j++;
}else{
i--;
}
}
if (i-left>0)
this.quickSort(left,i, a,a[left]);
if (right-j>0)
this.quickSort(j,right, a,a[j]);
}
let a = [34,12,56,12,46,112,8,6,17,34];
console.log("这是初始数据:"+a+"\n");
this.quickSort(0,a.length-1,a,a[0]);
这是初始数据:34,12,56,12,46,112,8,6,17,34
这是第1次排序:17,12,56,12,46,112,8,6,34,34
这是第2次排序:17,12,6,12,46,112,8,56,34,34
这是第3次排序:17,12,6,12,8,112,46,56,34,34
这是第4次排序:17,12,6,12,8,112,46,56,34,34
这是第5次排序:8,12,6,12,17,112,46,56,34,34
这是第6次排序:8,12,6,12,17,112,46,56,34,34
这是第7次排序:6,12,8,12,17,112,46,56,34,34
这是第8次排序:6,12,8,12,17,112,46,56,34,34
这是第9次排序:6,8,12,12,17,112,46,56,34,34
这是第10次排序:6,8,12,12,17,112,46,56,34,34
这是第11次排序:6,8,12,12,17,112,46,56,34,34
这是第12次排序:6,8,12,12,17,34,46,56,34,112
这是第13次排序:6,8,12,12,17,34,46,56,34,112
这是第14次排序:6,8,12,12,17,34,46,56,34,112
这是第15次排序:6,8,12,12,17,34,34,56,46,112
这是第16次排序:6,8,12,12,17,34,34,56,46,112
这是第17次排序:6,8,12,12,17,34,34,46,56,112
这是第18次排序:6,8,12,12,17,34,34,46,56,112
1.2插入排序
1.2.1直接插入排序
整体思路比较简单,就是二重循环进行遍历,插入排序,需要注意两点:
10. 里面的循环不要用let类定义j,否则在循环外部无法使用
11. 关于数组后移,我建议学习的时候不要使用数组的splice()方法来直接操作,看懂算法思想是关键。
insertSort:function () {
let a = [34,12,56,12,46,112,8,6,17,33];
console.log("这是初始数据:"+a+"\n");
for (let i = 1,n=1;i<a.length;i++,n++){//默认第一个元素是有序的
if (a[i]<a[i-1]){//该元素之前是有序的数据,如果它小于之前的元素,则需要插入,否则有序加1
let temp = a[i];
for (var j = i-1;j>=0;j--){//注意这里的j不能用let声明,因为需要所有元素后移,所以倒序更方便
if (a[j]>temp){//如果大于temp,则后移
a[j+1]=a[j];
}else{//结束不必要的循环,一点优化
break;
}
}
a[j+1]=temp;
console.log("这是第"+n+"次排序:"+a+"\n");
} else{//结束不必要的循环,一点优化
continue;
}
}
}
运行结果如下:
直接插入排序算法的时间复杂度是O(n2),空间负责度是O(1),需要注意的是直接插入排序在已是有序的数据中,时间复杂度是O(n),每次仅需要对比一次即可。
1.2.2 二分插入排序
二分插入排序使用了二分法思想,整体思路是不断的折半缩小取值范围,有点像大家在酒桌上玩的猜数字游戏,整体效率优于直接插入,但是不能在已是有序的数据中,算法的时间复杂度不变。
halfInsertSort:function () {
let a = [34,12,56,12,46,112,8,6,17,33];
console.log("这是初始数据:"+a+"\n");
for (let i = 1;i<a.length;i++){
let start = 0,end=i-1,temp = a[i];
if (a[i]<a[i-1]) {
while (end >= start) {
if (a[i] > a[Math.ceil((start + end) / 2)]) {
start = Math.ceil((start + end) / 2) + 1;
} else {
end = Math.ceil((start + end) / 2)-1;
}
}
for (let j = i - 1; j >= start; j--) {
a[j + 1] = a[j];
}
a[Math.ceil((start + end) / 2)] = temp;
console.log("这是第" + i + "次排序:" + a + "\n");
}
}
}
整体看着每次的结果和直接插入一样,但是在每次的循环体内,比较次数进行了优化,时间负责度是O(n(logn)),空间复杂度是O(1)
1.2.3希尔排序(shell排序)
希尔排序的优势是可以实现两个数据直接大幅度的跨越来进行比较和交换,减少了很多比较和移动操作。
shellSort:function () {
let a = [34,12,56,12,46,112,8,6,17,33];
console.log("这是初始数据:"+a+"\n");
//外层循环每次折半缩小间距
for (let state = Math.floor(a.length/2);state>0;state = Math.floor(state/2)){
for (let j = state;j<a.length;j++){
//注意此处间隔比较完成之后会一直向前比较,直至大于前者为止
for (let i=j-state;i>=0;i=i-state){
if (a[j]<a[i]){
[a[i],a[j]]=[a[j],a[i]];
}
}
}
console.log("这是第" + state + "次排序:" + a + "\n");
}
}
希尔排序的排序时间复杂度为O(n²),但是在最优的情况下能达到n*log2n。
1.3选择排序
1.3.1简单选择排序
选择排序的特点是是每次循环过程中只寻找操作目标,并不会执行操作,在选择之后进行交换操作。
choseSort:function () {
let a = [34,12,56,12,46,112,8,6,17,33];
console.log("这是初始数据:"+a+"\n");
for (let i = 0;i<a.length-1;i++){
let min =i;
for (let j = i+1;j<a.length;j++){//注意起点边界是i+1
if (a[min]>a[j]){
min = j;
}
}
if (i!=min)
[a[i],a[min]]=[a[min],a[i]];
console.log("这是第" + (i+1) + "次排序:" + a + "\n");
}
}
可以看到时间复杂度是O(n2),空间复杂度是O(1)
1.3.2 堆排序
堆排序是数据结构中的二叉树来进行排序,需要注意一下几点:
- 升序选用大根数,降序选用小根树
- 需要知道叶子节点的与其对应根节点下标的关系:Math.ceil(son/2)-1即为其对应根节点的下标(下标从零开始)
- 每次对树节点进行调整时,均需递归到底,保证调整之后的节点均满足根节点不小于或不大于其子节点
- 算法思路如下这个图画的挺细致,可以参考
: 因为没有使用结构体,我们直接把以为数组作为二叉树进行处理,其下标即为二叉树节点位置。
: 采用自左而右,自下而上的顺序来遍历,根据上面的描述,我们的第一个根节点是 Math.ceil(j / 2) - 1
: 之后进行递归的调用的我们的树节点调整函数
:最后交换根节点和最后一个数据的位置
数据源(本人用的vue测试的,所以就不贴调用代码了)
a:[34,12,56,12,46,112,8,6,17,33]
heapSort:function () {
console.log("这是初始数据:" + this.a + "\n");
for (let j = this.a.length-1; j >= 1; j--){
for (let i = Math.ceil(j / 2) - 1; i >= 0; i--) {
this.swap(i,j);
}
[this.a[0],this.a[j]]=[this.a[j],this.a[0]];
console.log("这是第" + (10-j) + "次交换后:" + this.a + "\n");
}
},
swap:function (i,j) {
if (i*2+2<=j&&this.a[i*2+2]>this.a[i]){//递归右子树
[this.a[i*2+2],this.a[i]]=[this.a[i],this.a[i*2+2]];
this.swap(i*2+2,j);
}
if (i*2+1<=j&&this.a[i*2+1]>this.a[i]){//递归左子树
[this.a[i*2+1],this.a[i]]=[this.a[i],this.a[i*2+1]];
this.swap(i*2+1,j);
}
}
运行结果如下
1.4 归并排序
1.4.1 二路归并排序
二路归并的思路是使用分治法,将有序的不同序列,通过归并的方式,合成一个有序的序列,其排序的效率仅次于快速排序。
//二路归并
doubleMerge:function(arr) {
if (arr&&arr.length===1){
return arr;
}else if (arr.length>0) {
let middle = Math.floor(arr.length/2);
//slice(start,end),截至到第end个元素,即下标为end-1
return
this.merge(this.doubleMerge(arr.slice(0,middle)),this.doubleMerge(arr.slice(middle)));
}
},
merge:function (left,right) {
let result = [];
console.log(left,right);
while(left.length>0&&right.length>0){
if (left[0]<right[0])
result.push(left.shift());
else{
result.push(right.shift());
}
}
return result.concat(left).concat(right);
}
因为使用了递归操作,所以看排序的过程从下往上看,通过递归,将待排序列拆分到最小,即有序序列(一个元素肯定是有序的),然后进行归并操作。
今天先到这,之后慢慢补全,以
上如果有误
,欢迎留言指正。