前端面试查漏补缺--(十四) 算法及排序

8,577 阅读17分钟

前言

本系列最开始是为了自己面试准备的.后来发现整理越来越多,差不多有十二万字符,最后决定还是分享出来给大家.

为了分享整理出来,花费了自己大量的时间,起码是只自己用的三倍时间.如果喜欢的话,欢迎收藏,关注我!谢谢!

文章链接

合集篇:

前端面试查漏补缺--Index篇(12万字符合集) 包含目前已写好的系列其他十几篇文章.后续新增值文章不会再在每篇添加链接,强烈建议议点赞,关注合集篇!!!!,谢谢!~

算法术语

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度: 运行完一个程序所需内存的大小。

时间复杂度和空间复杂度可以查看这篇文章: 时间复杂度和空间复杂度详解

数据结构

  • :一种遵从先进后出 (LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  • 队列:与上相反,一种遵循先进先出 (FIFO / First In First Out) 原则的一组有序的项;队列在尾部添加新元素,并从头部移除元素。最新添加的元素必须排在队列的末尾。
  • 链表:存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的;每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针/链接)组成。
  • 集合:由一组无序且唯一(即不能重复)的项组成;这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
  • 字典:以 [键,值] 对为数据形态的数据结构,其中键名用来查询特定元素,类似于 Javascript 中的Object
  • 散列:根据关键码值(Key value)直接进行访问的数据结构;它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度;这个映射函数叫做散列函数,存放记录的数组叫做散列表。
  • :由 n(n>=1)个有限节点组成一个具有层次关系的集合;把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的,基本呈一对多关系,树也可以看做是图的特殊形式。
  • :图是网络结构的抽象模型;图是一组由边连接的节点(顶点);任何二元关系都可以用图来表示,常见的比如:道路图、关系图,呈多对多关系。

更详细的解读可以查看这: 这篇文章这篇文章

排序对比:

图片名词解释: n: 数据规模 k:“桶”的个数 In-place: 占用常数内存,不占用额外内存 Out-place: 占用额外内存

排序分类:

关于排序算法的说明

  • 千万不要死记实现代码!
  • 记住算法动画
  • 通过动画,理解算法的思想和实现方法

只有这样你才能做到正在自信地在面试官面前手写代码,还能边写边和他讲解思路!

1.冒泡排序(Bubble Sort)

算法描述

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。因此取名冒泡排序.

冒泡排序

算法步骤及实现代码

思路: 冒泡排序属于基本排序算法,大致思路是两层循环嵌套.结合下面的动图,整理思路: 外循环遍历数组的每一项,确定两两比较循环的次数(其实最后一次可以省略),内循环则用于确定单次循环两两元素比较的次数,注意外层每循环一次,内循环两两比较的次数就会减1,即动图中的黄色块,表示已经排序好的柱形。

步骤:

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

JS代码实现:

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {        //相邻元素两两对比
			[arr[j],arr[j+1]] = [arr[j+1],arr[j]]  //通过解构完成元素交换
                
            }
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bubbleSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

说明:关于冒泡算法的其他实现思路:逆序,双向等实现方法,可以查看这篇文章 ,这里就不费笔墨了.

2.选择排序(Selection Sort)

算法描述

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序也是表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度.所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。 理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

选择算法

算法步骤及实现代码

思路: 选择排序也属于基本排序算法,大致思路也是两层循环嵌套.结合下面的动图和它的工作原理:首先外循环,每循环一次就确定了一个值在排序中的位置(动图中为从左依次确定).那要经过多少次,这样的循环?答案就是数列的长度减1. 接着是内循环: 确定剩下的未排序的柱形需要逐个比较的次数.

步骤: n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 1.初始状态:无序区为R[1..n],有序区为空;
  • 2.第i趟排序(i=1,2,3...n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • 3.n-1趟结束,数组有序化了。

JS代码实现:

function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
        minIndex = i;   //用来保存最小数
        for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {     //寻找最小的数
                minIndex = j;                 //将最小数的索引保存
            }
        }
		[arr[minIndex],arr[i]] = [arr[i],arr[minIndex]]  //通过解构完成元素交换
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(selectionSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

3.插入排序(Insertion Sort)

算法描述

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序核心--扑克牌思想: 就想着自己在打扑克牌,接起来第一张,放哪里无所谓,再接起来一张,比第一张小,放左边,继续接,可能是中间数,就插在中间.后面起的牌从后向前依次比较,并插入.

插入排序

算法步骤及实现代码

思路: 插入排序也属于基本排序算法,大致思路也是两层循环嵌套.首先,按照其扑克牌的思路.将要排序的数列分为两部分.左边为有序数列(起在手中的牌),刚开始为空.右边部分为待排序的数列(即乱序的扑克牌).

有了上面大致思想后,开始设置循环.首先外循环为你需要起多少张牌.那是多少?毫无疑问就是数列的长度,但是为了方便,我们可以默认让数列第一个数作为有序数列,可以减少一次循环.故外循环次数为数列长度减1;内循环则循环有序数列,并从右往左,比较大小,将较小数插在前面(结合动图)

步骤:

  • 1.从第一个元素开始,该元素可以认为已经被排序;
  • 2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 3.如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 5.将新元素插入到该位置后;
  • 6.重复步骤2~5。

JS代码实现:

function insertSort(arr) {
    for(let i = 1; i < arr.length; i++) {  //外循环从1开始,默认arr[0]是有序段
        for(let j = i; j > 0; j--) {  //j = i,表示此时你起在手上的那张牌,将arr[j]依次比较插入有序段中
            if(arr[j] < arr[j-1]) {
                [arr[j],arr[j-1]] = [arr[j-1],arr[j]];  //其实这里内循环中,只要比它前一个数小就交换,直到没有更小的,就break退出.这和动图表示的插入还是有点区别的,但最后结果其实是一样的.
            } else {
                break;
            }
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(insertSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

4.快速排序(Quick Sort)

算法描述

快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高! 它是处理大数据最快的排序算法之一了。

它是在冒泡排序基础上的递归分治法。通过递归的方式将数据依次分解为包含较小元素和较大元素的不同子序列。该算法不断重复这个步骤直至所有数据都是有序的。

注意: 快速排序也是面试是最最最容易考到的算法题,经常就会让你进行手写.

快速排序

算法步骤及实现代码

思路: 快速排序属于高级排序算法,此时就不是相似的循环嵌套.它的大概思想就是: 找到一个数作为参考,比这个数字大的放在数字左边,比它小的放在右边; 然后分别再对左边和右变的序列做相同的操作(递归).

注意: 涉及到递归的算法,一定要记得设置出口,跳出递归!

步骤:

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

JS代码实现:

function quickSort (arr) {
	if(arr.length <= 1) {
        return arr;  //递归出口
    }
	let left = [],
        right = [],
		//这里我们默认选择数组第一个为基准,PS:其实这样偷懒是不好的,如果数组是已经排好序了的.则很有可能变成最差情况的时间复杂度
		//pivotIndex = Math.floor(arr.length / 2),
	    pivot = arr[0];    //阮一峰版:  arr.splice(pivotIndex, 1)[0];   使用splice在大量数据时,会消耗大量内存;但也不至于被喷得一无是处! 它的思路是没有任何问题的! 
	for (var i = 1; i < arr.length; i++) {
		if (arr[i] < pivot) {
			left.push(arr[i])
		} else {
			right.push(arr[i])
		}
	}
	//concat也不适合大量数据的排序,会消耗大量内存
	return quickSort(left).concat(pivot, quickSort(right))
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

//改进版:
function partition2(arr, low, high) {
  let pivot = arr[low];
  while (low < high) {
    while (low < high && arr[high] > pivot) {
      --high;
    }
    arr[low] = arr[high];
    while (low < high && arr[low] <= pivot) {
      ++low;
    }
    arr[high] = arr[low];
  }
  arr[low] = pivot;
  return low;
}

function quickSort2(arr, low, high) {
  if (low < high) {
    let pivot = partition2(arr, low, high);
    quickSort2(arr, low, pivot - 1);
    quickSort2(arr, pivot + 1, high);
  }
  return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(quickSort2(arr,0,arr.length-1));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

5.希尔排序(Shell Sort)

算法描述

1959年Shell发明; 第一个突破O(n^2)的排序算法;是简单插入排序的改进版;它与插入排序的不同之处在于,它会优先比较距离较远的元素。 希尔排序又叫缩小增量排序.并且排序也是不稳定的

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

算法步骤及实现代码

思路: 希尔排序其实大体思路很简单,就是将数组(长度为len)分成间隔为t1的若干数组.进行插入排序;排完后,将数组再分成间隔为t2(逐步减小)的若干数组,进行插入排序;然后继续上述操作,直到分成间隔为1的数组,再进行最后一次插入排序则完成.

方便理解可以查看下图:

步骤:

  • 1,选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 2,按增量序列个数k,对序列进行k 趟排序;
  • 3,每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

JS代码实现:

function shellSort(arr) {
    var len = arr.length,
        temp,
        gap = 1;
    while(gap < len/5) {          //动态定义间隔序列
        gap =gap*5+1;
    }
    for (gap; gap > 0; gap = Math.floor(gap/5)) {
        for (var i = gap; i < len; i++) {
            temp = arr[i];
            for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
                arr[j+gap] = arr[j];
            }
            arr[j+gap] = temp;
        }
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(shellSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

6.归并排序(Merge Sort)

算法描述

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

归并排序

算法步骤及实现代码

思路: 将数组分为左和右两部分,然后继续将左右两部分继续(递归)拆分,直到拆分成单个为止;然后将拆分为最小的两个数组,进行比较,合并排成一个数组.接着继续递归比较合并.直到最后合并为一个数组.

步骤:

  • 1.把长度为n的输入序列分成两个长度为n/2的子序列;
  • 2.对这两个子序列分别采用归并排序;
  • 3.将两个排序好的子序列合并成一个最终的排序序列。

JS代码实现:

function mergeSort(arr) {  //采用自上而下的递归方法
    var len = arr.length;
    if(len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right){
    var result = [];
    while (left.length && right.length) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        } else {
            result.push(right.shift());
        }
    }

    while (left.length)
        result.push(left.shift());

    while (right.length)
        result.push(right.shift());
    return result;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(mergeSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

7.堆排序(Heap Sort)

算法描述

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

归并排序

步骤:

  • 1.将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区;
  • 2.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n];
  • 3.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

JS代码实现:


function buildMaxHeap(arr,len) {   // 建立大顶堆
    
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i,len);
    }
}

function heapify(arr, i,len) {     // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;

    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }

    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }

    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest,len);
    }
}

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

function heapSort(arr) {
    var len = arr.length;
    buildMaxHeap(arr,len);

    for (var i = arr.length-1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0,len);
    }
    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(heapSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

8.计数排序(Counting Sort)

算法描述

计数排序几乎是唯一一个不基于比较的排序算法, 该算法于1954年由 Harold H. Seward 提出. 使用它处理一定范围内的整数排序时, 时间复杂度为O(n+k), 其中k是整数的范围, 它几乎比任何基于比较的排序算法都要快( 只有当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序, 如归并排序和堆排序).

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。

计数排序

算法步骤及实现代码

思路: 计数排序利用了一个特性, 对于数组的某个元素, 一旦知道了有多少个其它元素比它小(假设为m个), 那么就可以确定出该元素的正确位置(第m+1位)

步骤:

  • 1, 获取待排序数组A的最大值, 最小值.
  • 2, 将最大值与最小值的差值+1作为长度新建计数数组B,并将相同元素的数量作为值存入计数数组.
  • 3, 对计数数组B累加计数, 存储不同值的初始下标.
  • 4, 从原数组A挨个取值, 赋值给一个新的数组C相应的下标, 最终返回数组C.

注意: 如果原数组A是包含若干个对象的数组,需要基于对象的某个属性进行排序,那么算法开始时,需要将原数组A处理为一个只包含对象属性值的简单数组simpleA, 接下来便基于simpleA进行计数、累加计数, 其它同上.

JS代码实现:

//以下实现不仅支持了数值序列的排序,还支持根据对象的某个属性值来排序。
function countSort(array, keyName){
  var length = array.length,
      output = new Array(length),
      max,
      min,
      simpleArray = keyName ? array.map(function(v){
        return v[keyName];
      }) : array; // 如果keyName是存在的,那么就创建一个只有keyValue的简单数组

  // 获取最大最小值
  max = min = simpleArray[0];
  simpleArray.forEach(function(v){
    v > max && (max = v);
    v < min && (min = v);
  });
  // 获取计数数组的长度
  var k = max - min + 1;
  // 新建并初始化计数数组
  var countArray = new Array(k);
  simpleArray.forEach(function(v){
    countArray[v - min]= (countArray[v - min] || 0) + 1;
  });
  // 累加计数,存储不同值的初始下标
  countArray.reduce(function(prev, current, i, arr){
    arr[i] = prev;
    return prev + current;
  }, 0);
  // 从原数组挨个取值(因取的是原数组的相应值,只能通过遍历原数组来实现)
  simpleArray.forEach(function(v, i){
    var j = countArray[v - min]++;
    output[j] = array[i];
  });
  return output;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(countSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

9.桶排序(Bucket Sort)

算法描述

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  • 在额外空间充足的情况下,尽量增大桶的数量
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

桶排序

算法步骤及实现代码

思路: 桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排

步骤:

  • 1.设置一个定量的数组当作空桶;
  • 2.遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 3.对每个不是空的桶进行排序;
  • 4.从不是空的桶里把排好序的数据拼接起来。

注意: 如果原数组A是包含若干个对象的数组,需要基于对象的某个属性进行排序,那么算法开始时,需要将原数组A处理为一个只包含对象属性值的简单数组simpleA, 接下来便基于simpleA进行计数、累加计数, 其它同上.

JS代码实现:

function bucketSort(arr, bucketSize) {
    if (arr.length === 0) {
      return arr;
    }

    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) {
      if (arr[i] < minValue) {
          minValue = arr[i];                // 输入数据的最小值
      } else if (arr[i] > maxValue) {
          maxValue = arr[i];                // 输入数据的最大值
      }
    }

    //桶的初始化
    var DEFAULT_BUCKET_SIZE = 5;            // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;   
    var buckets = new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = [];
    }

    //利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
        buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }

    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
        insertionSort(buckets[i]);                      // 对每个桶进行排序,这里使用了插入排序
        for (var j = 0; j < buckets[i].length; j++) {
            arr.push(buckets[i][j]);                      
        }
    }

    return arr;
}
var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48];
console.log(bucketSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

10.基数排序(Radix Sort)

算法描述

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

按照优先从高位或低位来排序有两种实现方案:

  • MSD: 由高位为基底, 先按k1排序分组, 同一组中记录, 关键码k1相等, 再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来, 便得到一个有序序列. MSD方式适用于位数多的序列.
  • LSD: 由低位为基底, 先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列. LSD方式适用于位数少的序列.

基数排序,计数排序,桶排序.这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

基数排序

算法步骤及实现代码

步骤:

  • 1.取得数组中的最大数,并取得位数;
  • 2.arr为原始数组,从最低位开始取每个位组成radix数组;
  • 3.对radix进行计数排序(利用计数排序适用于小范围数的特点);

JS代码实现:

/**
 * 基数排序适用于:
 *  (1)数据范围较小,建议在小于1000
 *  (2)每个数值都要大于等于0
 * @author xiazdong
 * @param  arr 待排序数组
 * @param  maxDigit 最大位数
 */
//LSD Radix Sort

function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    var counter = [];
    console.time('基数排序耗时');
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]== null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value = null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    console.timeEnd('基数排序耗时');
    return arr;
}
var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log(radixSort(arr,2)); //[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

二叉树和二叉查找树

树是一种非顺序数据结构,一种分层数据的抽象模型,它对于存储需要快速查找的数据非常有用。

现实生活中最常见的树的例子是家谱,或是公司的组织架构图.

一个树结构包含一系列存在父子关系的节点。每个节点都有一个父节点(除了顶部的第一个 节点)以及零个或多个子节点.

树常见结构/属性:

  • 节点
    • 根节点
    • 内部节点:非根节点、且有子节点的节点
    • 外部节点/页节点:无子节点的节点
  • 子树:就是大大小小节点组成的树
  • 深度:节点到根节点的节点数量
  • 高度:树的高度取决于所有节点深度中的最大值
  • 层级:也可以按照节点级别来分层

二叉树

二叉树,是一种特殊的树,即子节点最多只有两个,这个限制可以使得写出高效的插入、删除、和查找数据。在二叉树中,子节点分别叫左节点和右节点。

二叉树

二叉查找树

二叉查找树是一种特殊的二叉树,相对较小的值保存在左节点中,较大的值(或者等于)保存在右节点中,这一特性使得查找的效率很高,对于数值型和非数值型数据,比如字母和字符串,都是如此。 现在通过JS实现一个二叉查找树。

节点:

二叉树的最小元素是节点,所以先定义一个节点

function Node(data,left,right) {
    this.left = left;
    this.right = right;
    this.data = data;
    this.show = () => {return this.data}
}

这个就是二叉树的最小结构单元

二叉树

function BST() {
    this.root = null //初始化,root为null
}

BST初始化时,只有一个根节点,且没有任何数据。 接下来,我们利用二叉查找树的规则,定义一个插入方法,这个方法的基本思想是:

  1. 如果BST.root === null ,那么就将节点作为根节点
  2. 如果BST.root !==null ,将插入节点进行一个比较,小于根节点,拿到左边的节点,否则拿右边,再次比较、递归。

这里就出现了递归了,因为,总是要把较小的放在靠左的分支。换言之

最左变的叶子节点是最小的数,最右的叶子节点是最大的数

function insert(data) {
    var node = new Node(data,null,null);
    if(this.root === null) {
        this.root = node
    } else {
        var current = this.root;
        var parent;
        while(true) {
            parent = current;
            if(data < current.data) {
                current = current.left; //到左子树
                if(current === null) {  //如果左子树为空,说明可以将node插入在这里
                    parent.left = node;
                    break;  //跳出while循环
                }
            } else {
                current = current.right;
                if(current === null) {
                    parent.right = node;
                    break;
                }
            }
        }
    }
}

这里,是使用了一个循环方法,不断的去向子树寻找正确的位置。 循环和递归都有一个核心,就是找到出口,这里的出口就是当current 为null的时候,代表没有内容,可以插入。

接下来,将此方法写入BST即可:

function BST() {
    this.root = null;
    this.insert = insert;
}

这样子,就可以使用二叉树这个自建的数据结构了:

var bst = new BST();
bst.insert(10);
bst.insert(8);
bst.insert(2);
bst.insert(7);
bst.insert(5);
复制代码

但是这个时候,想要看树中的数据,不是那么清晰,所以接下来,就要用到遍历了。

树的遍历:

按照根节点访问的顺序不同,树的遍历分为以下三种:

  • 前序遍历 (根节点->左子树->右子树)
  • 中序遍历 (左子树->根节点->右子树)
  • 后序遍历 (左子树->右子树->根节点)

先序遍历:

先序遍历是以优先于后代节点的顺序访问每个节点的。先序遍历的一种应用是打印一个结构化的文档。

先序遍历

function preOrder(node) {
    if(node !== null) {
        //根节点->左子树->右子树
        console.log(node.show());
        preOrder(node.left);
        preOrder(node.right);
    }
}

中序遍历:

中序遍历是以从最小到最大的顺序访 问所有节点。中序遍历的一种应用就是对树进行排序操作。

中序遍历

function inOrder(node) {
    if(node !== null) {
        //如果不是null,就一直查找左变,因此递归
		//左子树->根节点->右子树
        inOrder(node.left);
        //递归结束,打印当前值
        console.log(node.show());
        //上一次递归已经把左边搞完了,右边
        inOrder(node.right);
    }
}

后序遍历:

后序遍历则是先访问节点的后代节点,再访问节点本身。后序遍历的一种应用是计算一个目录和它的子目录中所有文件所占空间的大小。

后序遍历

function postOrder(node) {
    if(node !== null) {
        //左子树->右子树->根节点
        postOrder(node.left);
        postOrder(node.right);
        console.log(node.show())
    }
}

二叉树的查找

在二叉树这种数据结构中进行数据查找是最方便的:

  • 最小值: 最左子树的叶子节点
  • 最大值: 最右子树的叶子节点
  • 特定值: target与current进行比较,如果比current大,在current.right进行查找,反之类似。

清楚思路后,就动手来写:

//最小值
function getMin(bst) {
    var current = bst.root;
    while(current.left !== null) {
        current = current.left;
    }
    return current.data;
}

//最大值
function getMax(bst) {
    var current = bst.root;
    while(current.right !== null) {
        current = current.right;
    }
    return current.data;
}

最大、最小值都是非常简单的,下面主要看下如何通过

function find(target,bst) {
    var current = bst.root;
    while(current !== null) {
        if(target === current.data) {
            return true;
        }
        else if(target > current.data) {
            current = current.right;
        } else if(target < current.data) {
            current = current.left;
        }
    }
    return -1;
}

其实核心,仍然是通过一个循环和判断,来不断的向下去寻找,这里的思想其实和二分查找是有点类似的。

AVL树:

AVL树是一种自平衡二叉搜索树,AVL树本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树),在AVL树中任何节点的两个子树的高度最大差别为一,也就是说这种树会在添加或移除节点时尽量试着成为一棵完全树,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是 O(log n),增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。

红黑树:

红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能;它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的 n 是树中元素的数目。

红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  • 节点是红色或黑色
  • 根节点是黑色
  • 每个叶节点(NIL节点,空节点)是黑色的
  • 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

这些约束强制了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用如即时应用(real time application)中有价值,而且使它们有在提供最坏情况担保的其他数据结构中作为建造板块的价值;例如,在计算几何中使用的很多数据结构都可以基于红黑树。 红黑树在函数式编程中也特别有用,在这里它们是最常用的持久数据结构之一,它们用来构造关联数组和集合,在突变之后它们能保持为以前的版本。除了O(log n)的时间之外,红黑树的持久版本对每次插入或删除需要O(log n)的空间。

感谢及参考

求内推

如果有好的前端岗位,欢迎内推我,谢谢。

本科,三年运维,五年前端。 base 武汉 邮箱 bupabuku@foxmail.com