# 前端的算法与数据结构

# 算法分类

十种常见排序算法可以分为两大类:

  • 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。
  • 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

相关概念

  1. 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  2. 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  3. 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  4. 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

# 深度优先遍历和广度优先遍历

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<div class="parent">
		  <div class="child-1">
		    <div class="child-1-1">
		      <div class="child-1-1-1">a</div>
		    </div>
		    <div class="child-1-2">
		      <div class="child-1-2-1">b</div>
		    </div>
		    <div class="child-1-3">c</div>
		  </div>
		  <div class="child-2">
		    <div class="child-2-1">d</div>
		    <div class="child-2-2">e</div>
		  </div>
		  <div class="child-3">
		    <div class="child-3-1">f</div>
		  </div>
		</div>
	</body>
</html>
<script type="text/javascript">
	var p=document.querySelector(".parent")
	// 深度遍历 方法一: 递归方式实现
	const deepTraversal1 = (node, nodeList = []) => {
	  if (node !== null) {
	    nodeList.push(node)
	    let children = node.children
	    for (let i = 0; i < children.length; i++) {
	      deepTraversal1(children[i], nodeList)
	    }
	  }
	  //console.log(1)
	  console.log(nodeList)
	  return nodeList
	}
	
	// var t1=deepTraversal1(p)
	// console.log(t1)
	
	// 深度遍历 方法二: 非递归方式实现
	const deepTraversal2 = (node) => {
	  let stack = []
	  let nodes = []
	  if (node) {
	    // 推入当前处理的node
	    stack.push(node)
	    while (stack.length) {
	      let item = stack.pop()
		  console.log(item)
	      let children = item.children
	      nodes.push(item)
	      // node = [] stack = [parent]
	      // node = [parent] stack = [child3,child2,child1]
	      // node = [parent, child1] stack = [child3,child2,child1-2,child1-1]
	      // node = [parent, child1-1] stack = [child3,child2,child1-2]
	      for (let i = children.length - 1; i >= 0; i--) {
	        stack.push(children[i])
	      }
	    }
	  }
	  return nodes
	}
	var t2=deepTraversal2(p)
	console.log(t2)
	
</script>

  • 广度算法
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<div class="parent">
		  <div class="child-1">
		    <div class="child-1-1">
		      <div class="child-1-1-1">a</div>
		    </div>
		    <div class="child-1-2">
		      <div class="child-1-2-1">b</div>
		    </div>
		    <div class="child-1-3">c</div>
		  </div>
		  <div class="child-2">
		    <div class="child-2-1">d</div>
		    <div class="child-2-2">e</div>
		  </div>
		  <div class="child-3">
		    <div class="child-3-1">f</div>
		  </div>
		</div>
	</body>
</html>
<script type="text/javascript">
	const widthTraversal = (node) => {
	  let nodes = []
	  let stack = []
	  if (node) {
	    stack.push(node)
	    while (stack.length) {
	      let item = stack.shift()
	      let children = item.children
	      nodes.push(item)
	        // 队列,先进先出
	        // nodes = [] stack = [parent]
	        // nodes = [parent] stack = [child1,child2,child3]
	        // nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2]
	        // nodes = [parent,child1,child2]
	      for (let i = 0; i < children.length; i++) {
	        stack.push(children[i])
	      }
	    }
	  }
	  console.log(nodes)
	  return nodes
	}
	
	var p=document.querySelector(".parent")
	widthTraversal(p)
	
</script>

# 排序

# 冒泡排序(Bubble Sort)

原理:比较两个相邻的元素,将值大的元素交换到右边

算法描述

  1. 比较第一位与第二位,如果第一位比第二位大,则交换位置
  2. 继续比较后面的数,按照同样的方法进行比较,到最后一位的时候,最大的数将被排在最后一位
  3. 重复进行比较,直到排序完成,注意由于上一次排序使得最后一位已经是最大的数,所以每次排序结束之后,下一次比较的时候可以相应的减少比较数量
let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
let flag;
let len = arr.length;
let num1 = 0; // 比较的次数
let num2 = 0; // 交换的次数
// 有15个数,只需要选出14个最大的数,最后一个数就是最小的,不用进行比较
for(let i = 0; i < len -1 ; i++){
	// 每次i变化之后最大的值已经排序到最后一位,无需对最后一位进行比较,所以j的最大值为len-i-1
	for(let j = 0; j < len - i - 1; j++){
		num1 += 1;
		// 如果当前位置的数比下一位置的数大,则交换位置
		if(arr[j] > arr[j+1]){
			num2++;
			flag = arr[j];
			arr[j] = arr[j+1];
			arr[j+1] = flag
		}
	}
}
console.log(arr,num1,num2);
//[1, 3, 8, 12, 12, 15, 16, 19, 22, 36, 45, 45, 56, 65, 96] 105 46

从代码中看出,排序过程中,所需要的临时变量一直都没有变化,因此空间复杂度为O(1);代码进行了两次for循环且是嵌套循环,因此时间复杂度为O(n²)。

冒泡排序的最优情况是原数组默认正序排序,此时比较的次数num1仍为105,而交换次数num2为0,此时的时间复杂度仍然为O(n²),那么为什么前面的复杂度表格中说是O(n)呢?经过一番研究发现,需要对上述代码进行简单优化。

如果排序的数组是:[1,2,3,4,5],此时完全符合最优复杂度情况,当我们进行第一次循环发现,两两相邻的数据一次都没有进行交换,也就是说所有的数都比前一个数大,此时就是正序,无需再进行下次排序,所以我们只需要加上一个变量进行判断:

    // 初始未产生交换
    let isSwap = false;
    for(let i = 0; i < len -1 ; i++){
        // 每次i变化之后最大的值已经排序到最后一位,无需对最后一位进行比较,所以j的最大值为len-i-1
        for(let j = 0; j < len - i - 1; j++){
            num1 += 1;
            // 如果当前位置的数比下一位置的数大,则交换位置
            if(arr[j] > arr[j+1]){
                num2 =+ 1;
                isSwap = true;
                flag = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = flag
           }
        }
        // 如果没有产生任何交换,则可以直接结束循环
        if(!isSwap){
            return
        }else{
			// 如果之前已经产生了交换,那么下次循环重新设置isSwap
			// 因为加入仅仅有一个数位置不对,没有这里他之后isSwap一直都是true
			// 就再也不能处理位置不变的情况了
			isSwap =false
		}
    }

# 选择排序

原理:将序列分为未排序和已排序,从未排序序列中找到最小的数,放到无序序列起始位置,然后继续从剩余未排序序列中继续寻找最小值

算法描述

初始无序序列为待排序序列,有序序列为空 从无序序列中找到最小值,放到无序序列起始位置,也就是和起始位置交换 将无序序列起始位向后推一个位置,继续2步骤

// 选择出无序序列中最小的值放到无序第一位
let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
let len = arr.length;
let minIndex;
let flag;
let num1 = 0; // 比较次数
let num2 = 0; // 交换次数
for(let i = 0; i < len - 1;i++){
	// 每次选择最小值之后,无序区的开始位置往后推1
	minIndex = i;
	// j循环到最后一位,选择出当前无序数组中数值最小的索引值
	for(let j = i + 1; j < len;j++){
		num1 += 1;
		if(arr[minIndex]>arr[j]){
			minIndex = j;
		}
	}
	num2 += 1;
	flag = arr[i];
	arr[i] = arr[minIndex];
	arr[minIndex] = flag;
	
}
console.log(arr,num1,num2)
//[1, 3, 8, 12, 12, 15, 16, 19, 22, 36, 45, 45, 56, 65, 96] 105 14
  • 为什么说选择排序是不稳定的呢?

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。

举个例子,序列arr = [5 8 5 2 9],我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法

# 插入排序

构建有序序列,对于未排序的数据,从有序序列后向前扫描,找到相应位置插入

let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
let len = arr.length;
// 定义当前未排序数据起始位置值也就是即将插入的数据
let currentValue;
// 有序序列遍历位置
let preIndex;
let num1 = 0; // 比较次数
let num2 = 0; // 交换次数
for(let i = 1;i < len; i++){
// 定义原始数据第二位为未排序数据第一位,默认原始数据第一位已排序
currentValue = arr[i];
// 当前有序序列最大索引
preIndex = i - 1;
// 当索引大于等于0且当前索引值大于需要插入的数据时
for(let j = preIndex;j>=0;j--){
	// 第一次比较,如果有序序列最大索引值其实就是待插入数据前一位,比待插入数据大,则后移一位
	num1+=1;
	console.log(arr[preIndex],preIndex)
	if(arr[preIndex]>currentValue){
		arr[preIndex+1] = arr[preIndex];
		preIndex --;
		// 索引减1,继续向前比较
	}else{
		break;
	}
}
// 当出现索引所在位置值比待插入数据小时,将待插入数据插入
// 为什么是preIndex+1,因为在循环里面后移一位之后,当前索引已经变化
num2 += 1;
arr[preIndex+1] = currentValue;
}
console.log(arr,num1,num2)

# 希尔排序

希尔排序将序列分割成若干小序列(逻辑上分组),对每一个小序列进行插入排序,此时每一个小序列数据量小,插入排序的效率也提高了。

# 算法描述

  1. 选择一个增量序列,t1,t2....tk,其中ti>tj,tk = 1;
  2. 按增量序列个数k,对序列进行k次排序;
  3. 每次排序,根据增量ti,将待排序序列分成若干子序列,分别对子序列进行直接插入排序。当增量为tk也就是1时,进行最后一次排序,此时子序列为排序序列本身。
    let arr = [3, 45, 16, 8, 65, 15, 36, 22, 19, 1, 96, 12, 56, 12, 45];
	let len = arr.length;
	let willInsertValue; 
	let gap = len; // 定义增量
	// 动态定义增量序列,每一次增量变为上次一半,最后一次的gap为1
	while(gap>0&&(gap = Math.trunc(gap/2))){
		// 对每个分组进行插入排序,为什么i开始是gap,因为插入排序默认第一位是已排序序列,arr[gap]是第一个分组第二位
		for(let i = gap;i<len;i++){
			// 待进行插入的值为a[i]
			willInsertValue = arr[i];
			// 按组进行插入,这里比较绕人,前面说了,只是逻辑上的分组,实际上还是一个序列,这里按组插入的时候是交叉
			// 进行插入
			let j = i - gap;
			// 下面就是个直接插入排序,只不过每次移位的时候下标差值为gap
			while(j>=0&&arr[j]>willInsertValue){
				arr[j+gap] = arr[j];
				j -= gap
			}
			arr[j+gap] = willInsertValue
		}
	}

	console.log(arr)
	//[1, 3, 8, 12, 12, 15, 16, 19, 22, 36, 45, 45, 56, 65, 96]

希尔排序是对直接插入排序的一种优化,可以用于大型的数组,希尔排序比插入排序和选择排序要快的多,并且数组越大,优势越大。

# 堆排序

  1. 每个元素arr【i】的节点为arr【2i+1】和arr【2i+2】。
  2. 最后一个父节点为arr.length/2-1.
function sort(arr){
		//初始化一个堆结构
		for(var i =parseInt(arr.length/2)-1;i>=0;i--){
			adHeap(i,arr,arr.length);
		}
		for(var j=arr.length-1;j>0;j--){
			//只有第一个元素被替换,剩下的满足堆排序的特点,所以从0开始就可以
			swap(arr,0,j);
			adHeap(0,arr,j)
		}
	}
	//实现堆结构
	function adHeap(i,arr,length){
		let temp = arr[i];
		for(var k=i*2+1;k<length;k=k*2+1){
			//找到大的那个节点,然后赋值给父节点
			if(k+1<length && arr[k]<arr[k+1]){
				k++;
			}
			if(arr[k]>temp){
				arr[i] = arr[k];
				i = k;
			}else{
				break;
			}
		}
		//再用父节点的元素给补充上
		arr[i] = temp;
	}

	function swap(arr,i,j){
		let empty = arr[i];
		arr[i] = arr[j];
		arr[j] = empty;
	}

	let arr = [4,1,2,3,5,0,9,20,6,1];
	sort(arr);
	console.log(arr);
最后更新: 4/26/2022, 2:45:18 PM