算法梳理之排序

前言:
我一直在犹豫要不要做一些算法的梳理,因为什么犹豫呢,就是因为算法与前端的重要性而言,其实没有那么重要,为什么,前端是展示在用户直面的区域的,那么前端实际上是要尽量优化一些计算的,前端也不善于处理数据,这根本原因在于我们的引擎是内嵌在浏览器的且注定我们只能去单线程跑代码逻辑
前端有很多的优越性,例如展示,这也是前端的核心,但是前端其实也有很多的局限性,这就导致了我们会尽量的去优化代码,减少代码量减少请求减少计算
我一直认为当一个前端把能抛弃的东西全部抛弃只做展示来用的时候才是最优的,当然随着前端的发展我们其实更加关注的应该是安全,交互,可维护性,可扩展性,但是我们进军计算机行业,专注做编程的人万万是不能抛弃算法的,因为计算机嘛,其实越靠近底层越发现,一切都是以计算为基础的
暂且不说我们在实际的业务需求中,有时候就是碰到了不得不把事情放在前端的场景,我们咬牙做的同时肯定要考虑到大量数据大量计算的时候前端的能力是否可以优化,我们还要知道,vue的虚拟dom是因为什么更加受人们的喜欢,就是因为diff算法啊,有了diff算法,次啊可以让性能更优,才可以让大多数的人去认可

其实算法总体来说要比设计模式更加具象,不再是一种理念,更像是一种固有的计算逻辑,今天来看看都有什么排序方法

  • 1.冒泡排序
1.冒泡排序
  • 我们只要是上过计算机课的,只要是有基础的同学应该都会知道的一种算法,冒泡排序的理念也跟好理解,因为计算机是固定格式的计算,那我们就给一种固定格式的判断进行固定格式的操作,冒泡就是对相邻的元素进行两两比较,顺序相反则进行交换,这样,进行n-1次循环,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序
  • 我用比较通俗的话解释一下,其实就是把第一个数跟第二个数对比,谁大的谁放后边,然后第二个跟第三个比,第三个跟第四个比,一个回合比下来,到倒数第二个数跟倒数第一个数比较的时候,就会把这组数里最大的数排到最后一位,第二次循环依旧进行这样的比较,筛选出第二个最大的,且第二次比较就可以剔除最后一位数了,这样比较到n-1次的时候只剩下两个数,大的排第二位之后,第一位自然就是最小的了,这也是n-1次循环的原因,我们来看下代码
const arr = [1, 20, 10, 30, 22, 11, 55, 24, 31, 88, 12, 100, 50];

function bubbleSort(arr){
  for(let i = 0; i < arr.length - 1; i++){
    for(let j = 0; j < arr.length - i - 1; j++){
      if(arr[j] > arr[j + 1]){
        swap(arr, j);
      }
    }
  }
  return arr;
}

function swap(arr, j){
  let temp = arr[j];
  arr[j] = arr[j+1];
  arr[j+1] = temp;
}

我们可以看到,代码中有两个方法
第一个方法中调用了第二个方法,那我们先来说第二个方法的作用,交换数值,接收三个参数,一个是数组,一个是下标,作用就是把下边为j的数值和下标的后一个数值做一个交换,具体交换规则很简单,找一个介质,先把下标的值存进去,然后把后一个的值存在这里来,然后把存的值放进后一个下标的位置
第二个方法就是冒泡排序了,只不过在循环的比较的时候,把数值交换位置这样的能力拿出来封装成了第二个方法,我们进行了两次for循环,第一次的for循环就是在循环排序的次数,我们可以看到循环了length-1次,然后第二个for循环看到每次循环的时候我们把整个数组进行的一次最大值的冒泡行为,而且根据length-i-1可以看出,每次冒泡的队列都在忽略上次循环已经冒泡出来的数,最终就可以达到目的,我们也可以反着来,从最后一位开始比较,这样就是反向冒泡

2.选择排序
  • 选择排序的概念:第一个概念就是从第一个数到最后一个数依次固定位置,第二个概念就是尽量减少数据位置的移动,每次就只进行最小值下标的记录,如果当前要固定的值不是最小值,选出未固定的剩余数值中最小的值跟当前要固定的值做交换
  • 通俗来讲就是我从第一个数开始,跟剩下的数做比较,记录下标,如果发现第三个数比当前的要小,那就记下是第几个,然后拿这个记住的数跟后边的数作比较,直到这轮比较结束,我记录了一个最小数的下标,然后把这个下标跟第一个数交换位置,然后第二个,找到了跟第二个交换位置,依次循环,一直到倒数第二个,因为第二个一固定位置无疑最后一个肯定是最大的
  • 选择排序是不稳定的,为啥是不稳定的很多人有疑惑,举个栗子就是说有 2 2 1三个数进行排序第一次比较会把第三个跟第一个交换位置,第二次因为俩数相同所以不叫唤位置,虽然最终结果是1 2 2,但是这个结果实际上跟我们预期的不太相符,我们预期的是第一三个的1在两个2前边,但是并不希望两个2的顺序是颠倒的
function selectionSort(arr) {
	var minIndex, temp;
	for (var i = 0; i < arr.length - 1; i++) {
		minIndex = i;
		for (var j = i + 1; j < arr.length; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j;
			}
		}
		temp = arr[i];
		arr[i] = arr[minIndex];
		arr[minIndex] = temp;
	}
	return arr;
}

从以上代码来看,(我们就直接把交换逻辑写在里边了)循环的逻辑我就不说了,重点就是minIndex的下标位置记录,每次循环记录最小的那个下标,跟当前数做位置交换
两个地方需要注意,一个就是里边的for循环的时候我们不再是length-i,而是j=i+1,原因就是我们说的,我们固定的值实际上是从顺序开始的,当然了我们也可以改造,每次选择出最大的值放在最后边,然后每次固定的数值从最后一位开始固定,重要的只是选择这个思想,另一个就是我们定义的变量,都放在了for循环的外边,因为这些变量是可以复用的,不需要反复重新定义变量

3.快速排序
  • 听名字我们就知道,这个排序就突出一个字,快,时间复杂度最低,排序进度最快,基本概念是:选择一个基数,这个基数可以选择数组中的第一个数或者最后一个数,因为我们要递归拆分,所以最终的数组会二分到只有一个数甚至没有,给定两个容器,把比基数大的放在前边的容器,把基数小的放在后边的容器,然后对两个容器进行同样方法的拆分排序
  • 通俗点来讲,就是我把所有的数像二叉树一样的掰开,然后选中两个叉里边的一个基数,继续根据大小掰开,这样一直分下去,那就会产生一个效果,在拆分到最小的时候,比如只剩三个数甚至两个数的时候,就区分出来哪个大哪个小了,然后再递归的拼接起来返回回去,就排好了一个有序的序列

这个我觉得我有必要模拟一下情况,我就不画图了

                         [5,3,7,2,1,6,9,8,4]//原始的数组
            [3,2,1,4]              5             [7,6,9,8]//把第一个数5当作基数,拆成俩数组,前边放小的后边放大的
       [2,1]     3      [4]        5       [6]       7         [9,8]//继续拆,中数依旧放着
    [1]  2  []   3  []   4   []    5   []   6    []  7    [8]    9    [] //继续拆,空数组的判断拆分结束
 [] 1 [] 2  []   3  []   4   []    5   []   6    []  7  [] 8 []  9    [] //当所有拆分值都无法拆分的时候,拆分结束,开始递归

这个图就是表达一个拆分的思想,无限评分然后再收集拼接就是快排的算法

const arr = [5, 3, 7, 2, 1, 6, 9, 8, 4];

function quickSort(arr){
  if(arr.length <= 1){
    return arr;
  }
  let temp = arr[0];
  const left = [];
  const right = [];
  for(var i = 1; i < arr.length; i++){
    if(arr[i] > temp){
      right.push(arr[i]);
    }else{
      left.push(arr[i]);
    }
  }
  return quickSort(left).concat([temp], quickSort(right));
}

console.log(quickSort(arr));

递归我就不说了吧,因为递归其实不算是算法,不进行数据计算,只是一种代码执行的逻辑,通过递归我们可以去回归拼接那些通过多次二分法去拆分的数组,quickSort(left).concat([temp], quickSort(right))这个就是核心了,我们可以看到拼接的时候是,把左右两个数组容器再次进行方法调用,等待返回值且跟基数,也就是中数,进行一个数组的拼接

4.插入排序
  • 插入排序的概念就是:从第一位开始确认顺序,依次往后,确认位置的方法就是向前查找是否有比自己小的数,有的话就插在这个数的后边,
  • 通俗的讲就是我从第二个开始看第一个比不比我小,比我小插第二个后边,比我大插第一个前边,第三个的时候跟当前的第二个比,如果第二个比我小我就插在第二个的后边,因为前边的大小顺序已经确定了,所以比第二个大一定比第一个大,如果比第二个小,先不急插入,继续向前找看比不比第一个小,如果比第一个小插第一个的前边,后续依旧按照这个逻辑插入
function Insertion(arr) {
  let len = arr.length;
  let preIndex, current;
  for (let i = 1; i < len; i++) {
    preIndex = i - 1;
    current = arr[i];
    while (preIndex >= 0 && current < arr[preIndex]) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex--;
    }
    arr[preIndex + 1] = current;
  }
  return arr;
}
 
 
var arr = [3,5,7,1,4,56,12,78,25,0,9,8,42,37];
Insertion(arr);

网上有人说很像插扑克牌的逻辑,其实有一点点差别,就是计算机不会一眼看到哪个是最小的如果一个笨人从第一张去挨个往后看然后插扑克牌,那才是插入排序,真正的看扑克牌的逻辑反而更像是选择排序

5.二路归并
  • 二路归并是处理两个排好序的数组进行合并的,其实我们在前端更多用的方法是先合并再排序,其实归并排序比先合并再排序复杂度要低一些,算法方面耗时也更少一些,因为归并排序的概念是挨个比较两个数组的最小值,谁小的谁先进新的数组容器,最终就会是一个合并好的排好序的数组
  • 这里我没讲传统意义的归并排序,是我觉得这个归并排序实在是不好,要进行完全拆分然后再两两二路归并,复杂度很高很不友好
function merge(left, right) {
    let arr = [];
    while(left.length > 0 && right.length > 0){
        if(left[0] > right[0]) {
            arr.push(right.shift())
        }else{
            arr.push(left.shift())
        }
    }
    return arr.concat(left, right);
}

其实这个算法我依旧有疑惑,我看了很多的文章,都是再说归并排序,我没get到归并排序的优点在哪里,稳定吗?拆分一个无序的数组,无限拆分,然后再进行以上的merge也就是二路归并操作,对计算机来讲真的是空间复杂度和事件复杂度会降低吗,不会啊我感觉,归并排序的理念并不是很难理解,但是我找不到优点,反而是下边的这个merge操作还是有说法的,合并起来感觉会提高性能,但是也只局限于有序的子数组的合并使用

算法是计算机的基础,我们在实际应用中很难用到这样的手写的算法处理逻辑,但是还是那句话,学习理念,举一反三,理念明白了到别的地方也都能用得到,不一定是要在前端处理数据的时候用到,因为前端我们已有的方法非常的多,当然是用不到的,就算处理复杂逻辑也会有插件供我们使用,但是算法,依旧有必要

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值