JavaScript数据结构之排序和搜索算法

JavaScript数据结构之排序和搜索算法

排序算法

在开始排序算法之前,先创建一个数组(列表)来表示待排序和搜索的数据结构。

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

ArrayList 是一个简单的数据结构,它将项存储在数组中(行 {1} )。只需要一个插入方法来向数据结构中添加元素(行 {2} ),为了验证结果, toString 方法使用JavaScript原生 Array 类的 join方法,来拼接数组中的所有元素至一个单一的字符串,这样就可以在控制台输出结果了。

冒泡排序

运行时间的角度来看,冒泡排序是最差的一个。
冒泡排序比较任何两个相邻的项,如果第一个比第二个大,则交换它们。元素项向上移动至正确的顺序,就好像气泡升至表面一样。

实现冒泡排序:

this.bubbleSort = function(){
	var length = array.length; //{1}
	for (var i=0; i<length; i++){ //{2}
		for (var j=0; j<length-1; j++ ){ //{3}
			if (array[j] > array[j+1]){ //{4}
				swap(j, j+1); //{5}
			}
		}
	}
};

首先,声明一个名为 length 的变量,用来存储数组的长度(行 {1} )。这一步可选,它能在行 {2} 和行 {3} 时直接使用数组的长度。接着,外循环(行 {2} )会从数组的第一位迭代至最后一位,它控制了在数组中经过多少轮排序(应该是数组中每项都经过一轮,轮数和数组长度一致)。

然后,内循环将从第一位迭代至倒数第二位,内循环实际上进行当前项和下一项的比较(行 {4} )。如果这两项顺序不对(当前项比下一项大),则交换它们(行 {5} ),意思是位置为j+1 的值将会被换置到位置 j 处,反之亦然。

现在得声明 swap 函数(一个私有函数,只能用在 ArrayList 类的内部代码中):

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

交换时,用一个中间值来存储某一交换项的值。
注意:其他排序法也会用到这个方法,因此声明一个方法放置这段交换代码以便重用。
下面这个示意图展示了冒泡排序的工作过程:
在这里插入图片描述
该示意图中每一小段表示外循环的一轮(行 {2} ),而相邻两项的比较则是在内循环中进行的(行 {3} )。
将使用下面这段代码来测试冒泡排序算法,看结果是否和示意图所示一致:

function createNonSortedArray(size){ //{6}
	var array = new ArrayList();
	for (var i = size; i> 0; i--){
		array.insert(i);
	}
	return array;
}
var array = createNonSortedArray(5); //{7}
console.log(array.toString()); //{8}
array.bubbleSort(); //{9}
console.log(array.toString()); //{10}

为了辅助测试本章将要学习的排序算法,将创建一个函数来自动地创建一个未排序的数组,数组的长度由函数参数指定(行 {6} )。如果传递 5 作为参数,该函数会创建如下数组: [5, 4,3, 2, 1] 。

调用这个函数并将返回值存储在一个变量中,该变量将包含这个以某些数字来初始化的 ArrayList 类实例(行 {7} )。在控制台上输出这个数组内容,确保这是一个未排序数组(行 {8} ),接着调用冒泡排序方法(行 {9} )并再次在控制台上输出数组内容以验证数组已被排序了(行 {10} )。

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

扫描二维码关注公众号,回复: 10156098 查看本文章

改进后的冒泡排序
如果从内循环减去外循环中已跑过的轮数,就可以避免内循环中所有不必要的比较(行{1} )。

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

下面这个示意图展示了改进后的冒泡排序算法是如何执行的:
在这里插入图片描述

选择排序

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

下面是选择排序算法的源代码:

this.selectionSort = function(){
	var length = array.length, //{1}
	indexMin;
	for (var i=0; i<length-1; i++){ //{2}
		indexMin = i; //{3}
		for (var j=i; j<length; j++){ //{4}
			if(array[indexMin]>array[j]){ //{5}
				indexMin = j; //{6}
			}
		}
		if (i !== indexMin){ //{7}
			swap(i, indexMin);
		}
	}
};

首先声明一些将在算法内使用的变量(行 {1} )。接着,外循环(行 {2} )迭代数组,并控制迭代轮次(数组的第n个值——下一个最小值)。假设本迭代轮次的第一个值为数组最小值(行{3} )。

然后,从当前 i 的值开始至数组结束(行 {4} ),比较是否位置 j 的值比当前最小值小(行 {5} );如果是,则改变最小值至新最小值(行 {6} )。当内循环结束(行 {4} ),将得出数组第n小的值。最后,如果该最小值和原最小值不同(行 {7} ),则交换其值。

用以下代码段来测试选择排序算法:

array = createNonSortedArray(5);
console.log(array.toString());
array.selectionSort();
console.log(array.toString());

下面的示意图展示了选择排序算法,此例基于之前代码中所用的数组。
数组底部的箭头指示出当前迭代轮寻找最小值的数组范围(内循环 {4} ),示意图中的每一步则表示外循环。
选择排序同样也是一个复杂度为O(n 2 )的算法。和冒泡排序一样,它包含有嵌套的两个循环,这导致了二次方的复杂度。然而,接下来要学的插入排序比选择排序性能要好。
在这里插入图片描述

插入排序

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

下面这段代码表示插入排序算法:

this.insertionSort = function(){
	var length = array.length, //{1}
	j, temp;
	for (var i=1; i<length; i++){ //{2}
		j = i; //{3}
		temp = array[i]; //{4}
		while (j>0 && array[j-1] > temp){ //{5}
			array[j] = array[j-1]; //{6}
			j--;
		}
		array[j] = temp; //{7}
	}
};

照例,算法的第一行用来声明代码中使用的变量(行 {1} )。接着,迭代数组来给第i项找到正确的位置(行 {2} )。
注意,算法是从第二个位置(索引 1 )而不是 0 位置开始的(默认第
一项已排序了)。
然后,用 i 的值来初始化一个辅助变量(行 {3} )并将其值亦存储于一临时变量中(行 {4} ),便于之后将其插入到正确的位置上。下一步是要找到正确的位置来插入项目。只要变量 j 比 0 大(因为数组的第一个索引是 0 ——没有负值的索引)并且数组中前面的值比待比较的值大(行 {5} ),就把这个值移到当前位置上(行 {6} )并减小 j 。最终,该项目能插入到正确的位置上。
在这里插入图片描述
举个例子,假定待排序数组是 [3, 5, 1, 4, 2] 。这些值将被插入排序算法按照下面形容的步骤进行排序。

  • 3已被排序,所以我们从数组第二个值5开始。3比5小,所以5待在原位(数组的第二位)。3和5排序完毕。
  • 下一个待排序和插到正确位置上去的值是1(目前在数组的第三位)。5比1大,所以5被移至第三位去了。我们得分析1是否应该被插入到第二位——1比3大吗?不,所以3被移到第二位去了。接着,我们得证明1应该插入到数组的第一位上。因为0是第一个位置且没有负数位,所以1必须被插入到第一位。1、3、5三个数字已经排序。
  • 4应该在当前位置(索引3)还是要移动到索引较低的位置上呢?4比5小,所以5移动到索引3位置上去。那么应该把4插到索引2的位置上去吗?4要比3大,所以4插入到数组的位置3上。
  • 下一个待插入的数字是2(数组的位置4)。5比2大,所以5移动至索引4。4比2大,所以4也得移动(位置3)。3也比2大,所以3还得移动。1比2小,所以2插入到数组的第二位置上。至此,数组已排序完成。

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

归并排序

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

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

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

this.mergeSort = function(){
	array = mergeSortRec(array);
};

mergeSort 方法将会调用 mergeSortRec ,该函数是一个递归函数:

var mergeSortRec = function(array){
	var length = array.length;
	if(length === 1) { //{1}
		return array; //{2}
	}
	var mid = Math.floor(length / 2), //{3}
	left = array.slice(0, mid), //{4}
	right = array.slice(mid, length); //{5}
	return merge(mergeSortRec(left), mergeSortRec(right)); //{6}
};

归并排序将一个大数组转化为多个小数组直到只有一个项。由于算法是递归的,需要一个停止条件,在这里此条件是判断数组的长度是否为 1 (行 {1} )。

如果是,则直接返回这个长度为 1 的数组(行 {2} ),因为它已排序了。

如果数组长度比 1 大,那么得将其分成小数组。为此,首先得找到数组的中间位(行 {3} ),找到后将数组分成两个小数组,分别叫作 left (行 {4} )和 right (行 {5} )。

left 数组由索引0至中间索引的元素组成,而 right 数组由中间索引至原始数组最后一个位置的元素组成。
下面的步骤是调用 merge 函数(行 {6} ),它负责合并和排序小数组来产生大数组,直到回到原始数组并已排序完成。为了不断将原始数组分成小数组,得再次对 left 数组和 right 数组递归调用 mergeSortRec ,并同时作为参数传递给 merge 函数。

var merge = function(left, right){
	var result = [], // {7}
	il = 0,
	ir = 0;
	while(il < left.length && ir < right.length) { // {8}
		if(left[il] < right[ir]) {
			result.push(left[il++]); // {9}
		} else{
			result.push(right[ir++]); // {10}
		}
	}
	while (il < left.length){ // {11}
		result.push(left[il++]);
	}
	while (ir < right.length){ // {12}
		result.push(right[ir++]);
	}
	return result; // {13}
};

merge 函数接受两个数组作为参数,并将它们归并至一个大数组。排序发生在归并过程中。

首先,需要声明归并过程要创建的新数组以及用来迭代两个数组( left 和 right 数组)所需的两个变量(行 {7} )。迭代两个数组的过程中(行 {8} ),比较来自 left 数组的项是否比来自 right数组的项小。

如果是,将该项从 left 数组添加至归并结果数组,并递增迭代数组的控制变量(行{9} );否则,从 right 数组添加项并递增相应的迭代数组的控制变量(行 {10} )。接下来,将 left数组或者 right 数组所有剩余的项添加到归并数组中。最后,将归并数组作为结果返回。

如果执行 mergeSort 函数,下图是具体的执行过程:
在这里插入图片描述
可以看到,算法首先将原始数组分割直至只有一个元素的子数组,然后开始归并。归并过程也会完成排序,直至原始数组完全合并并完成排序。

快速排序

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

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

快速排序的实现:

this.quickSort = function(){
	quick(array, 0, array.length - 1);
};

就像归并算法那样,开始声明一个主方法来调用递归函数,传递待排序数组,以及索引0及其最末的位置(因为要排整个数组,而不是一个子数组)作为参数。

var quick = function(array, left, right){
	var index; //{1}
	if (array.length > 1) { //{2}
		index = partition(array, left, right); //{3}
		if (left < index - 1) { //{4}
			quick(array, left, index - 1); //{5}
		}
		if (index < right) { //{6}
			quick(array, index, right); //{7}
		}
	}
};

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

如果数组的长度比1大(因为只有一个元素的数组必然是已排序了的(行 {2} ),将对给定子数组执行 partition 操作(第一次调用是针对整个数组)以得到 index (行 {3} )。

如果子数组存在较小值的元素(行 {4} ),则对该数组重复这个过程(行 {5} )。同理,对存在较大值得子数组也是如此,如果存在子数组存在较大值,也将重复快速排序过程(行 {7} )。

1. 划分过程
var partition = function(array, left, right) {
	var pivot = array[Math.floor((right + left) / 2)], //{8}
	i = left, //{9}
	j = right; //{10}
	while (i <= j) { //{11}
		while (array[i] < pivot) { //{12}
			i++;
		}
		while (array[j] > pivot) { //{13}
			j--;
		}
		if (i <= j) { //{14}
			swapQuickStort(array, i, j); //{15}
			i++;
			j--;
		}
	}
	return i; //{16}
};

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

另外一种方式是随机选择一个数组项或是选择中间项。在本实现中,选择中间项作为主元(行 {8} )。

初始化两个指针: left (低——行 {9} ),初始化为数组第一个元素;right (高——行 {10} ),初始化为数组最后一个元素。只要 left 和 right 指针没有相互交错(行 {11} ),就执行划分操作。首先,移动 left 指针直到找到一个元素比主元大(行 {12} )。

对 right 指针,做同样的事情,移动 right 指针直到找到一个元素比主元小。当左指针指向的元素比主元大且右指针指向的元素比主元小,并且此时左指针索引没有右指针索引大(行 {14} ),意思是左项比右项大(值比较)。交换它们,然后移动两个指针,并重复此过程(从行 {11} 再次开始)。
在划分操作结束后,返回左指针的索引,用来在行 {3} 处创建子数组。

swapQuickStort 函数和前面实现的 swap 函数十分相似。唯一的不同之处是发生交换值的的数组同样也是函数的参数。

var swapQuickStort = function(array, index1, index2){
	var aux = array[index1];
	array[index1] = array[index2];
	array[index2] = aux;
};
2. 快速排序实战

给定数组 [3, 5, 1, 6, 4, 7, 2] ,前面的示意图展示了划分操作的第一次执行。
下面的示意图展示了对有较小值的子数组执行的划分操作(注意7和6不包含在子数组之内):
在这里插入图片描述
接着,继续创建子数组,请看下图,但是这次操作是针对上图中有较大值的子数组(有1的那个较小子数组不用再划分了,因为它仅含有一个项)。
在这里插入图片描述
子数组( [2, 3, 5, 4] )中的较小子数组( [2, 3] )继续进行划分(算法代码中的行 {5} ):
在这里插入图片描述
然后子数组( [2, 3, 5, 4] )中的较大子数组( [5, 4] )也继续进行划分(算法中的行{7} ),示意图如下:
在这里插入图片描述
最终,较大子数组 [6, 7] 也会进行划分( partition )操作,快速排序算法的操作执行完成。

搜索算法

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

以下是其实现:

this.sequentialSearch = function(item){
	for (var i=0; i<array.length; i++){ //{1}
		if (item === array[i]) //{2}
			return i; //{3}
		}
	}
	return -1; //{4}
};

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

假定有数组 [5, 4, 3, 2, 1] 和待搜索值3,下图展示了顺序搜索的示意图:
在这里插入图片描述

二分搜索

二分搜索算法的原理和猜数字游戏类似,“想一个1到100的数字”的游戏。每回应一个数字,那个人就会说这个数字是高了、低了还是对了。
这个算法要求被搜索的数据结构已排序。以下是该算法遵循的步骤。

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

以下是其实现:

this.binarySearch = function(item){
	this.quickSort(); //{1}
	var low = 0, //{2}
	high = array.length - 1, //{3}
	mid, element;
	while (low <= high){ //{4}
		mid = Math.floor((low + high) / 2); //{5}
		element = array[mid]; //{6}
		if (element < item) { //{7}
			low = mid + 1; //{8}
		} else if (element > item) { //{9}
			high = mid - 1; //{10}
		} else {
			return mid; //{11}
		}
	}
	return -1; //{12}
};

开始前需要先将数组排序,这选择了快速排序。在数组排序之后,设置 low (行 {2} )和 high (行 {3} )指针(它们是边界)。当 low 比 high 小时(行 {4} ),计算得到中间项索引并取得中间项的值,此处如果 low 比
high 大,则意思是该待搜索值不存在并返回 -1 (行 {12} )。

接着,比较选中项的值和搜索值(行 {7} )。如果小了,则选择数组低半边并重新开始。如果选中项的值比搜索值大了,则选择数组高半边并重新开始。若两者都是不是,则意味着选中项的值和搜索值相等,因此,直接返
回该索引(行 {11} )。
在这里插入图片描述

发布了118 篇原创文章 · 获赞 257 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/low666/article/details/105054323