本章将会学习最常见的排序和搜索算法,如冒泡排序、选择排序、插入排序、归并排序、快速排序和堆排序,以及顺序排序和二叉搜索算法。

第十章 排序和搜索算法

排序算法

我们会从一个最慢的开始,接着是一些性能好一些的方法

先创建一个数组(列表)来表示待排序和搜索的数据结构。

function ArrayList(){
var array = [];
this.insert = function(item){
array.push(item);
}
this.toString = function(){
return array.join();
}
}

ArrayList 是一个简单的数据结构,它将项存储在数组。我们只需要一个插入方法来向数据结构中添加元素。使用js原生的push方法即可,而改写toString函数运用了js的join方法是来拼接数组中的所有元素至一个单一的字符串。

冒泡排序

冒泡排序是所有排序算法中最简单,然后从运行时间看,它也是最差的一个、

冒泡排序比较两个相邻的项,如果第一个大于第二个,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样,冒泡排序因此得名。

// 冒泡排序
this.bubbleSort = function(){
var length = array.length;
for(var i = 0; i <length; i++){
for(var j = 0; j < length -1; j--){
if(array[j] > array[j+1]){
this.swap(j,j+1);
}
}
}
}

首先,声明一个 名为length的变量,用来存储数组的长度。接着外循环从数组的第一位迭代到最后一位,它控制了在数组中经过多次轮排序,然后内循环将从第一位迭代到倒数第二位,内循环实际上进行当前项和下一项的比较。如果这两项顺序不对,则交换它们,意思就是位置为 j+1 的会被换到位置 j 处。

声明 swap函数

this.swap = function(index1,index2){
var aux = array[index1];
array[index1] = array[index2];
array[index2] = aux;
// [array[index1],array[index2]] = [array[index2],array[index1]]
}

交换时,我们用一个中间值来存储某一交换项的值。其他排序法也会用到这个方法,因此我们声明一个方法放置这段交换代码以便重用。

还可以简化成

[array[index1],array[index2]] = [array[index2],array[index1]]

下面这个示意图展示了冒泡排序的工作过程

上面的图每一小段表示外循环的一轮,而相邻两项的比较是在内循环中进行的。

用下面的代码来测试冒泡排序算法

function createNonSortedArray(size){
var array = new ArrayList();
for(var i = size; i > 0; i--){
array.insert(i);
}
return array;
} var array = createNonSortedArray(5);
console.log(array.toString()); // 5,4,3,2,1
array.bubbleSort();
console.log(array.toString()); // 1,2,3,4,5

为了辅助本章将要学习的排序算法,我们将创建一个函数来自动地创建一个未排序的数组,数组的长度由函数的参数指定。如果传递5为参数,该函数就会创建如下数组

[5,4,3,2,1]

调用这个函数并将返回值存储在一个变量中,该变量将包含这个以某些数字来初始化的 ArrayList 类实例。

注意当算法执行外循环的第二轮的时候,数字4和5已经是正确排序的了。但是在后续的比较中,它们还是在一直进行着比较,即使这是不必要的。因此我们稍微改进一下。

改进版冒泡排序

// 改进后的冒泡排序
this.modifiedBubbleSort = function(){
var length = array.length;
for(var i = 0; i <length; i++){
for(var j = 0; j < length-1-i; j++){
if(array[j] > array[j+1]){
this.swap(j,j+1);
}
}
}
}

下图展示了改进后的冒泡排序算法是如何执行的:

可以通过检验知道减少了10次循环,优化了算法的性能。

即使做了这样子的改变,还是不推荐该算法,该算法的复杂度是O(n2 )

后面的章节会介绍大O表达法。

选择排序

选择排序算法是一种原址比较排序算法。选择排序大致的思路是找到数据结构中最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位。以此类推。

实现:

// 选择排序
this.selectionSort = function(){
var length = array.length,
indexMin;
for(var i = 0; i < length; i++){
indexMin = i;
for(var j = i; j < length; j++){
if(array[indexMin] > array[j]){
indexMin = j;
}
}
if(i !== indexMin){
this.swap(i,indexMin);
}
}
}

首先声明一些将在算法内使用的变量。接着,外循环迭代数组,并控制迭代一次(数组的第n个值——下一个最小值)。我们假设本迭代一次的第一个值为数组的最小值。然后,当前 i 的值开始至数组结束,我们比较是否位置 j的值比当前的最小值小。如果是则改变最小值为新的最小值。当内循环结束,将得出数组的第n小的值。最后,如果该最小值和原最小值不一样,则交互其值。

测试:

var array = createNonSortedArray(5);
console.log(array.toString()); // 1,2,3,4,5
array.selectionSort();
console.log(array.toString()); // 1,2,3,4,5

下图的示意图展示了选择排序算法,此例基于之前的代码中所用的数组。

数组底部的箭头指示出了当前迭代寻找最小值的数组范围,示意图中的每一步则表示外部循环。

选择排序同样是一个复杂度也是 O(n2)的算法。和冒泡排序一样,它包含有嵌套的两个循环,这导致了二次方的复杂度。然后,接下来要学的插入排序比选择排序的性能要好。

插入排序

插入排序每次排一个数组项,以此方式构建最后的排序数组。假设第一项已经排序,接着,它和第二项进行比较,第二项是应该待在原位还是插入到第一项之前呢?这样,头两项就已经正确排序,接着和第三项比较(它是该插入到第一、第二还是第三的位置呢?),以此类推

实现:

// 插入排序
this.insertionSort = function(){
var length = array.length,
j,
temp;
for(var i = 0; i <length; i++){
j = i;
temp = array[i];
while(j > 0 && array[j - 1] > temp){
array[j] = array[j - 1]
j--;
}
array[j] = temp;
}
}

先声明代码中使用的变量,接着,迭代数组来给第i项找到正确的位置。注意,算法是从第二个位置而不是从0位置开始的。然后用i值来初始化一个辅助变量并将其保存于一临时变量中,便于之后将其插入到正确的位置上。下一步是找到正确的位置来插入项目。只要变量j比0大并且数组中前面的值比待比较的值大,我们就把这个值移到当前位置上并减小j。最终,该项目能插入到正确的位置上。

下面的示意图展示了一个插入排序的实例:

排序小型数组时,此算法比选择排序和冒泡排序性能都要好。

归并排序

归并排序是第一个可以被实际使用的排序算法。归并排序性能复杂度为 O(nlog(n))

归并算法是一种分治算法。其思想是将原始数组切成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后一个排序完毕的大数组。

由于是分治法,归并排序也是递归的。

// 归并排序
this.mergeSort = function(){
array = this.mergeSortRec(array);
}

mergeSortRec 是递归函数

this.mergeSortRec = function(array){
var length = array.length;
if(length === 1){
return array;
}
var mid = Math.floor(length/2),
left = array.slice(0,mid),
right = array.slice(mid,length);
return merge(arguments.callee(left),arguments.callee(right));
}

归并排序将一个大数组转化为一个小数组直到只有一个项。由于算法是递归的,我们需要一个停止条件,在这里条件是判断数组的长度是否为1.如果是,则直接返回这个长度为1的数组,因为它已经排序了。

如果数组长度比1大,那么我们得将其分成小数组。为此,首先要找到数组的中间行,找到后我们将数组分成两个小数组,分别叫做left 和 right 。 left 数组由索引0至中间索引的元素组成,而 right 数组由中间索引至原始数组最后一个位置的元素组成。

下面的步骤就是调用merge 函数,它负责合并和排序小数组来产生大数组,直到回到原始数组已排序完成。为了不断将原始数组分成小数组,我们得再次对left 数组和right 数组递归调用 mergeSortRec ,并同时作为参数传递给 merge 函数

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

merge 函数接受两个数组作为参数,并将它们归并至一个大数组。排序发生在归并过程中。首先,需要声明归并过程要创建的新数组已经用来迭代两个数组(left和right数组)所需要的两个变量。迭代两个数组的过程中,我们来自left 数组的项是否比来自right的数组的项小。如果是,将该项从left数组添加至归并结果数组,并递增迭代数组的控制变量;否则,从right数组添加项并递增相应的迭代数组的控制变量。

接下来,将left数组或right数组所有剩下的项添加到归并数组中。最后,将归并数组作为结果返回。

下图是具体的执行过程

可以看到,算法首先将原始数组分割成只有一个元素的子数组,然后开始排序。归并过程也会完成排序,知道原始数组完全合并并完成排序。

快速排序

快速排序也许是最常用的排序算法了。它的复杂度为O(nlogn),且它的性能通常比其他的复杂度为O(nlog(n))的排序算法要好。和归并算法一样,快速排序也是用分治 的方法,将原始算法分成了较小的数组(但它没有像归并排序那样将它们分割开)

快速排序比目前之前的排序算法要负责一些。

  1. 首先,从数组中选择中间一项作为主元
  2. 创建两个指针,左边一个指向数组的第一项,右边一个指向数组最后一项。移动左指针知道我们找到一个比主元大的元素,接着,移动右指针直到找到一个比主元小的元素,然后交换它们,重复这个过程,直到左指针超过了右指针。这个过程将使得比主元小的值都排在主元之前,而比主元大的值都排在主元之后。这一步叫做划分操作。
  3. 接着,算法对划分后的小数组(较主元小的值组成的子数组,以及较主元大的值组成的子数组)重复之前的两个步骤,直到数组已完全排序。

实现:

// 快速排序
this.quickSort = function(){
this.quick(array,0,array.length-1);
}

像归并算法那样,开始我们声明一个主方法来调用递归函数,传递待排序数组,已经索引0及其最末的位置作为参数。

this.quick = function(array,left,right){
var index;
if(array.length > 1){
index = partition(array,left,right);
if(left < index - 1 ){
arguments.callee(array,left,index-1);
}
if(index < right){
arguments.callee(array,index,right);
}
}
}

首先声明 index ,该变量能帮助我们将子数组分离成较小值数组和较大值数组,这样,我们就能再次递归的调用quick函数了。partition 函数返回值将赋值给 index.

如果数组的长度比1大,我们就对给定子数组执行 partition 操作以得到 index 。如果子数组存在较小值的元素,则对该数组重复这个过程。同理,对存在较大值的子数组也是如此。

划分过程

第一件要做的事情就是选中主元(pivot),有好几种方式,最简单的一种就是选中数组的第一项(最左项)。然而,研究表明对于几乎已排序的数组,这不是一个好的选择,它将导致该算法的最差表现。另外一种方式是随机选择一个数组或是选择中间项。

function partition(array,left,right){
var pivot = array[Math.floor((left+right) / 2)],
i = left,
j = right;
while(i <= j){
while(array[i] < pivot){
i++
}
while(array[j] > pivot){
j--
}
if(i <= j){
[array[i],array[j]] = [array[j],array[i]];
i++;
j--
}
}
return i;
}

上面的实现中,我们选中中间项作为主元。我们初始化两个指针:left,初始化为数组第一个元素,right,初始化为数组最后一个元素。

只要left和right指针没有相互交错,就执行划分操作。首先,先移动left指针直到找到一个元素比主元大。对于right指针,我们做同样的事情,移动right指针直到我们找到一个元素比主元小。

当左指针指向的元素比主元大且右指针指向的元素比主元小,并且此时左指针索引没有右指针索引大,意思是左项比右项大。我们交换它们,然后移动两个指针,并重复这个过程。

在划分操作结束后,返回左指针的索引,用来在创建子数组。

快速排序实战

看一个快速排序的实际例子

给定数组([3,5,1,6,4,7,2]),前面的示意图展示了划分操作的第一次执行。

下面的示意图展示了对有较小值的子数组执行的划分(注意7和6不包含在子数组之内)

接着,我们继续创建子数组,但是这次操作是针对上图中有较大值的子数组(有1那个较小数组不用再划分了,因为它仅含有一个项)

子数组([2,3,5,4])中的较小数组([2,3])继续划分。

然后子数组([2,3,5,4])中较大数组([5,4])也继续进行划分,示意图如下

最终,较大子数组([6,7])也会进行继续划分操作,快速排序算法的操作执行完成。

计数排序、桶排序和基数排序(分布式排序)

目前为止,已经学习了如何不借助任何辅助数据结构的情况下对数组进行排序。还有一类被称为分布式排序的算法,原始数组中的数据会分发到多个中间结构(桶),再合起来放回原始数组。

最著名的分布式算法有计数排序、桶排序和基数排序。这里不做展开,有兴趣的请自行百度。

排序的相关代码

function createNonSortedArray(size){
var array = new ArrayList();
for(var i = size; i > 0; i--){
array.insert(i);
}
return array;
}
function merge(left,right){
var result = [],
il = 0,
ir = 0;
while(il < left.length && ir < right.length){
if(left[il] < right[ir]){
result.push(left[il++]);
}else{
result.push(right[ir++]);
}
}
while(il < left.length){
result.push(left[il++]);
}
while(ir < right.length){
result.push(right[ir++]);
}
return result;
}
function partition(array,left,right){
var pivot = array[Math.floor((left+right) / 2)],
i = left,
j = right;
while(i <= j){
while(array[i] < pivot){
i++
}
while(array[j] > pivot){
j--
}
if(i <= j){
[array[i],array[j]] = [array[j],array[i]];
i++;
j--
}
}
return i;
}
function ArrayList(){
var array = [];
this.insert = function(item){
array.push(item);
}
this.toString = function(){
return array.join();
}
this.swap = function(index1,index2){
var aux = array[index1];
array[index1] = array[index2];
array[index2] = aux;
// [array[index1],array[index2]] = [array[index2],array[index1]]
}
// 冒泡排序
this.bubbleSort = function(){
var length = array.length;
for(var i = 0; i <length; i++){
for(var j = 0; j < length -1; j++){
if(array[j] > array[j+1]){
this.swap(j,j+1);
}
}
}
}
// 改进后的冒泡排序
this.modifiedBubbleSort = function(){
var length = array.length;
for(var i = 0; i <length; i++){
for(var j = 0; j < length-1-i; j++){
if(array[j] > array[j+1]){
this.swap(j,j+1);
}
}
}
}
// 选择排序
this.selectionSort = function(){
var length = array.length,
indexMin;
for(var i = 0; i < length; i++){
indexMin = i;
for(var j = i; j < length; j++){
if(array[indexMin] > array[j]){
indexMin = j;
}
}
if(i !== indexMin){
this.swap(i,indexMin);
}
}
}
// 插入排序
this.insertionSort = function(){
var length = array.length,
j,
temp;
for(var i = 0; i <length; i++){
j = i;
temp = array[i];
while(j > 0 && array[j - 1] > temp){
array[j] = array[j - 1]
j--;
}
array[j] = temp;
}
}
// 归并排序
this.mergeSort = function(){
array = this.mergeSortRec(array);
}
this.mergeSortRec = function(array){
var length = array.length;
if(length === 1){
return array;
}
var mid = Math.floor(length/2),
left = array.slice(0,mid),
right = array.slice(mid,length);
return merge(arguments.callee(left),arguments.callee(right));
}
// 快速排序
this.quickSort = function(){
this.quick(array,0,array.length-1);
}
this.quick = function(array,left,right){
console.log(left+' '+right);
var index;
if(array.length > 1){
index = partition(array,left,right);
if(left < index - 1 ){
arguments.callee(array,left,index-1);
}
if(index < right){
arguments.callee(array,index,right);
}
}
}
} // var array = createNonSortedArray(5);
// console.log(array.toString());
// array.quickSort();
// console.log(array.toString());
const quickSortArray = new ArrayList();
quickSortArray.insert(3);
quickSortArray.insert(5);
quickSortArray.insert(1);
quickSortArray.insert(6);
quickSortArray.insert(4);
quickSortArray.insert(7);
quickSortArray.insert(2);
console.log(quickSortArray.toString());
quickSortArray.quickSort();
console.log(quickSortArray.toString());

搜索算法

回顾一下之前学过的算法,我们会发现BinarySearch Tree 类的search以及LinkedList类的indexOf 方法等都是搜索算法。当然,它们都是根据各自的数据结构来实现的。所以我们其实已经熟悉两个搜索算法了,只是还不知道它们的正式名称而已。

顺序搜索

顺序或者线性搜索是基本的搜索算法。它的机制是,将每一个数据结构中的元素和我们要找的元素做比较。顺序搜索是最低效的一种搜索算法。

// 顺序搜索
this.sequentialSearch = function(item){
for(var i = 0; i < array.length; i++){
if(item === array[i]){
return i;
}
}
return -1;
}

顺序搜索迭代整个数组,并将每个数组元素和搜索项作比较。如果搜索到了,算法将返回值来标示搜索成功。返回值可以是该搜索项本身,或是true,又或是搜索项的索引。如果没有找到该项,则返回-1,表示该索引不存在,也可以考虑返回false或者null。

假定有数组([5,4,3,2,1])和待搜索值3,下图展示了顺序搜索的示意图

二分搜索

二分搜索算法的原理和菜数字游戏类似。我们每回应一个数字。那个人就会说这个数字是高了还是低了或者是对了。

这个算法要求被搜索的数据结构已经排序。以下是该算法遵循的步骤

  1. 选择数组的中间值
  2. 如果选中值是待搜索值,那么算法执行完毕
  3. 如果待搜索值比选中的小,则返回步骤1并在选中值的左边的子数组中寻找
  4. 如果待搜索值比选中的大,则返回步骤1并在选中值的右边的子数组中寻找

实现:

// 二分搜索
this.binarySearch = function(item){
this.quickSort();
var low = 0,
high = array.length - 1,
mid,element;
while(low <= high){
mid = Math.floor((low + high)/2);
element = array[mid];
if(element < item){
low = mid + 1;
}else if(element > item){
high = mid - 1;
}else{
return mid;
}
}
return -1;
}

开始前需要先排序数组,我们这这里选择了快速排序。在数组排序之后,我们设置low和high指针(它们是边界)

当low比high小时,我们计算得到中间项索引并取得中间项的值,此处如果low比high大,则意思是该搜索值不存在并返回-1.接着,我们比较选中项的值和搜索值。如果小了,则选择数组低半边并重新开始。如果选中项的值比搜索值大了,则选择数组高半边并重新开始。若两者都不是,则意味着选中项的值和搜索值相等,因此,直接返回该索引。

给定下图所示数组,试试搜索2.这是算法将会执行的步骤:

小结

本章介绍了排序和搜索算法,包括冒泡、选择、插入、归并和快速排序,还有顺序搜索和二分搜索。下一章学习一些高级算法技巧。

书籍链接: 学习JavaScript数据结构与算法

为什么我要放弃javaScript数据结构与算法(第十章)—— 排序和搜索算法的更多相关文章

  1. 为什么我要放弃javaScript数据结构与算法(第十一章)—— 算法模式

    本章将会学习递归.动态规划和贪心算法. 第十一章 算法模式 递归 递归是一种解决问题的方法,它解决问题的各个小部分,直到解决最初的大问题.递归通常涉及函数调用自身. 递归函数是像下面能够直接调用自身的 ...

  2. 为什么我要放弃javaScript数据结构与算法(第九章)—— 图

    本章中,将学习另外一种非线性数据结构--图.这是学习的最后一种数据结构,后面将学习排序和搜索算法. 第九章 图 图的相关术语 图是网络结构的抽象模型.图是一组由边连接的节点(或顶点).学习图是重要的, ...

  3. 为什么我要放弃javaScript数据结构与算法(第八章)—— 树

    之前介绍了一些顺序数据结构,介绍的第一个非顺序数据结构是散列表.本章才会学习另一种非顺序数据结构--树,它对于存储需要快速寻找的数据非常有用. 本章内容 树的相关术语 创建树数据结构 树的遍历 添加和 ...

  4. 为什么我要放弃javaScript数据结构与算法(第七章)—— 字典和散列表

    本章学习使用字典和散列表来存储唯一值(不重复的值)的数据结构. 集合.字典和散列表可以存储不重复的值.在集合中,我们感兴趣的是每个值本身,并把它作为主要元素.而字典和散列表中都是用 [键,值]的形式来 ...

  5. 为什么我要放弃javaScript数据结构与算法(第六章)—— 集合

    前面已经学习了数组(列表).栈.队列和链表等顺序数据结构.这一章,我们要学习集合,这是一种不允许值重复的顺序数据结构. 本章可以学习到,如何添加和移除值,如何搜索值是否存在,也可以学习如何进行并集.交 ...

  6. 为什么我要放弃javaScript数据结构与算法(第五章)—— 链表

    这一章你将会学会如何实现和使用链表这种动态的数据结构,这意味着我们可以从中任意添加或移除项,它会按需进行扩张. 本章内容 链表数据结构 向链表添加元素 从链表移除元素 使用 LinkedList 类 ...

  7. 为什么我要放弃javaScript数据结构与算法(第四章)—— 队列

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第四章 队列 队列数据结构 队列是遵循FIFO(First In First Out,先进先出,也称为先来先服务)原则的一组有序 ...

  8. 为什么我要放弃javaScript数据结构与算法(第三章)—— 栈

    有两种结构类似于数组,但在添加和删除元素时更加可控,它们就是栈和队列. 第三章 栈 栈数据结构 栈是一种遵循后进先出(LIFO)原则的有序集合.新添加的或待删除的元素都保存在栈的同一端,称为栈顶,另一 ...

  9. 为什么我要放弃javaScript数据结构与算法(第二章)—— 数组

    第二章 数组 几乎所有的编程语言都原生支持数组类型,因为数组是最简单的内存数据结构.JavaScript里也有数组类型,虽然它的第一个版本并没有支持数组.本章将深入学习数组数据结构和它的能力. 为什么 ...

随机推荐

  1. HDU ACM 2895-Edit distance

    Edit distance Time Limit : 2000/1000ms (Java/Other)   Memory Limit : 32768/32768K (Java/Other) Total ...

  2. July 21st 2017 Week 29th Friday

    If you want to fly too high in relation to the horizon forget. 要想飞得高,就该把地平线忘掉. Always keep our eyes ...

  3. CSAPP Bomb Lab记录

    记录关于CSAPP 二进制炸弹实验过程 (CSAPP配套教学网站Bomb Lab自学版本,实验地址:http://csapp.cs.cmu.edu/2e/labs.html) (个人体验:对x86汇编 ...

  4. WAS缓存问题

    在项目中经常遇见这样的问题:修改应用的配置文件web.xml后,无论重启应用还是重启WebSphere服务器,都不能重新加载web.xml,导致修改的内容无效. 这个问题困扰了我好久,即使删除了${w ...

  5. fastTime格式化时间

    { field: 'alarmTime', title: '告警时间', align: 'center', width: 90, valign: 'middle', formatter:functio ...

  6. checkout 提示失败

    事故现场有些时候,当我们使用 git checkout <branchname>命令切换分支的时候,有时会切换失败,然后出现以下的提示信息: 提示信息说的很清楚,当前分支有未跟踪的文件,c ...

  7. 手工检测SQL注入(安全性测试)

    手动你的ASP站可否注入: http://127.0.0.1/xx?id=11 and 1=1 (正常页面) http://127.0.0.1/xx?id=11 and 1=2 (出错页面) 检测表段 ...

  8. mysql的入门基本使用(适合新手学习)

    登陆Mysql数据库mysql -u root -p 展示(查询)所有数据库show databases;进入数据库use dataname;创建数据库create database dataname ...

  9. redis主从架构的搭建

    本项目采用主从架构,一主两从一个哨兵.在x.x.x.69上部署主节点,在70上部署从节点1和哨兵节点,在71上部署从节点2. 准备: 1.首先上传redis文件到三台linux上,目录/home/sy ...

  10. angular动态绑定样式以及改变UI框架样式的方法

    一:angular动态绑定样式 举个栗子: <tr *ngFor="let dataTr of tableData;let i = index" [formGroupName ...