前端媛需要知道的数据结构和简单排序算法---不能再少了

358 阅读12分钟

下面的内容针对每种数据结构详细介绍,针对每种数据结构都会列出常遇到的经典问题和实现方法,主要是从JS角度实现,不过只要思路明白,至于到底用什么语言,在本文中并不是那么重要了

数据结构

  • 数组

对于数组,是吾里编程人最熟悉的数据结构了,还记得学生时代经常拿数组和链表比较

说的最多的就是两点:查询和插入删除。当你需要高频做插入删除时,选择链表;需要高频查询时,选择数组。

  1. 数组插入删除:插入删除一项,首先需要找到满足具体条件的位置,然后当你插入删除一项时需要移动其他所有项,这样才能空出或填满插入删除的位置,这样就动员了所有项,高频的做这些操作,性能上远没有链表直接修改next。
  2. 链表查询:若想要查询某个具体位置的节点,需要从头节点依次遍历;而数组有下标,可以直接访问该下标的某项。

常见的数组问题

  1. 查找数组中第k小的元素

利用快排思想,left[] (<) right[],从小到大排序,若left里面个数m<k,则需要在right[]中找第k-m项,否则在left[]中找第k项

实现:

//查找数组中第K小的元素
var kArr = function(arr,k){
	if(arr.length < k){console.log("没有这么多数呀!");return}
	//结合快速排序
	var pivotIndex = Math.floor(arr.length / 2);
	var pivot = arr.splice(pivotIndex, 1)[0];
	var left = [];
	var right = [];
	for (var i = 0; i < arr.length; i++){
		if (arr[i] < pivot) {
			left.push(arr[i]);
		} else {
		right.push(arr[i]);
		}
    }
	if(left.length<k){
		return right[k-left.length];
	}
	else{
		return left[k];
	}
}

var arr = [1,3,2,5,11,43,22,77,45,12];

console.log(kArr(arr,6));

//这里为什么会选择快速排序思想结合实现,因为相比其他的简单排序而言,快速排序效率更高。
//具体的快速排序实现思想可见下面的排序分析内容

  1. 查找第一个没有重复的数组元素

实现:(1)直接用两层for循环(2)扫一遍数组,用map统计每个元素出现的次数val,再返回第一个val为1的项。

说到上面的第二个方法,笔者内心咯噔了一下,之前参加秋招,面试小姐姐问了一个简单算法问题,问我有什么优化的地方,我当时想复杂了,完全没往对象方向想,痛哭流涕,还是得多刷题才能找到感觉-.-

//查找第一个没有重复的元素
var oneArr = function(arr){
	if(arr.length === 1){return arr[0]};
	var obj = {};
	arr.forEach(element => {
		if(!obj[element]) obj[element] = 1;
		else obj[element]++;
	});
	for(var key in obj){
		if(obj[key] === 1) {
			return key;
		}
	}
}

var arr = [1,3,2,5,11,3,22,77,45,1,77,0,0];

console.log(oneArr(arr));

  1. 合并两个排序好的数组

实现:

对于JS实现很简单,直接concat后sort就好了,如果不用这些方法,可以用两个指针分别指向两个数组,让两个元素进行比较,把小的放到新数组中,并使较小的元素的数组指针加1,继续比较,直到有一个数组遍历完,最后,把另一个数组剩下的元素放到新数组后即可。

  1. 重新排列数组中的正数和负数

实现:

利用快速排序思想,将整数和负数分别放到right[]和left[]中,然后各自排序,最后concat

栈的主要核心:先进后出;栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。

递归函数的实现就利用了栈这种数据结构,当一个递归函数被调用时,被调函数的局部变量、形参的值以及一个返回地址就会储存在递归工作栈中。运行时按照后进先出的顺序,进行函数执行,完成递归操作。

  1. 使用栈计算后缀表达式

编译原理中,我们利用栈的结构特性实现后缀表达式的计算。

例:中缀表达式a + b * c + ( d * e + f ) * g,转化为后缀表达式之后是a b c * + d e * f + g * +

具体的转换过程:

1)如果遇到操作数,直接将其输出

2)如果遇到操作符,则将其放入栈中,遇到左括号也将其放入栈中

3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止,左括号只弹出不输出

4)遇到其他的操作符例如 + ,* , (从栈中弹出元素直到遇到发现更低优先级的元素或者栈空为止。弹出这些元素之后,才能将遇到的操作符压入到栈中,有一点要注意,只有遇到 ) 的情况下才弹出 ( 其他情况下都不会弹出)

5)如果读到了输入的末尾,则将栈中的所有元素依次弹出

  1. 使用栈为栈中的元素排序

实现:先通过js-class实现栈结构,然后借用辅助栈help实现栈stack的排序

class Vect{
    constructor(){
        this.stack = [];
    }
    //入栈
    in(num){
        if (typeof num != "number") return false;
        this.stack.push(num); 
    }

    //出栈
    out(){
        if(this.stack.length>0){
            let last = this.stack.pop();
            return last;
        }
    }

    //输出
    print(){
        if(this.stack.length === 0){
            console.log("栈空了");
        }
        else{
            console.log(...this.stack);
        }
    }
}

//利用辅助栈对存储栈排序
var sort = function(stack){
    var help = new Vect();
    while(stack.stack.length){
        var pop = stack.out();
        if(help.stack.length&&help.stack[help.stack.length-1]<pop) {//里面的判断顺序不能颠倒,否则出现 java.util.EmptyStackException
            stack.in(help.out());//当满足help不为空,且help的元素小于pop(这样排出的顺序顶到底是从小到大的)         
        }                      //将help里的元素返回到stack中

        help.in(pop);//无论什么情况,只要stack不为空,都将pop压入help
    }
    while(help.stack.length) {//当help不为空的时候,help里面的元素顶到底是从小到大的,
        stack.in(help.out());//所以将help弹到stack中是顶到底是从大到小的
    }
    stack.print();
}

var stack = new Vect();
stack.in(2);
stack.in(1);
stack.in(5);
sort(stack);

详细排序过程:

  1. in入栈stack[2,1,5],创建空栈help
  2. stack弹出pop=5,由于此时help为空,直接in入help栈
  3. 接着,stack弹出pop=1,由于1<5,直接in入help栈(保证help栈是底-顶:大-小)
  4. 再,stack弹出pop=2,由于1<2,将help中pop=1直接in入栈stack,然后将2入栈help中
  5. 继续,stack弹出pop=1,由于2>1,直接in入help栈
  6. 最后将help全都弹出放到stack中即有序的栈生成。

help栈

help
stack栈
tack

  1. 检查字符串中括号是否匹配正确

这里为了简化,直接就判断()是否匹配,若有需要其他符号,可以增加判断

实现思路:

实现过程中,默认先"("后")",若最开始遇到的事")",则直接跳出,显示"右括号多了"。

  1. 扫描str
  2. 遇到"(",入栈stack
  3. 遇到")",判断stack,若为空,右括号多了,返回false;若不为空,判断top栈顶,若不为左括号,则不匹配,返回false;若为左括号,出栈,继续下一个
  4. 扫描结束后,判断stack栈中是否为空,若不为空则说明还有左括号没有匹配完,左括号多了,返回false;否则匹配成功,返回true
//判断()是否匹配
var match = function(str){
    var strStack = new Vect();
    //扫描str
    var strArr = str.split('');
    var p = 0;
    while(p<strArr.length){
        if(strArr[p]==="("){
            strStack.in(strArr[p]);
        }
        if(strArr[p]===")"){
            if(strStack.stack.length===0){
                console.log("右括号多了");
                return false;
            }
            else if(strStack.stack[strStack.stack.length-1]!=="("){
                console.log("不匹配");
                return false;;
            }
            else{
                strStack.out();
            }
        }
        p++;
        
        
    }
    //结束后,如果栈中还有,表示有左括号没匹配完
    if(strStack.stack.length){
        console.log("左括号多于右括号");
        return false;
    }
    else{
        console.log("左右括号匹配正确");
        return true;
    }
}

match("((a+b)*v)/2)");
  • 队列

队列刚好和栈相反,核心即先进先出,实现方法和栈类似,区别上就是入队和出队的顺序问题,不赘述。

  • 链表

链表就是通过node和next连起来的一条链,本文中就简单介绍单链表的结构实现和相关问题的实现

  1. JS实现单链表结构,实现链表添加、删除、查找、反转
//结点
class Node{
	constructor(element){
		this.element = element;
		this.next = null;
	}
}
//链表
class LinkedList{
	constructor(){    //构造函数
		this.length = 0;
		this.head = null;
	}
	append(element){  //追加结点
		let node = new Node(element);
		let current;
		if(this.head == null) this.head = node;
		else{
				current = this.head;
				while(current.next){
					current = current.next;
				}
				current.next = node;
		}
		this.length++;
	}
	removeAt(position){   //删除指定位置的结点
		if(position >-1 && position < this.length){
			let current = this.head;
			let index = 0;
			let previous;
			if(position == 0){
				this.head = current.next;
			}else{
				while(index++ < position){
					previous = current;
					current= current.next;
				}
				previous.next = current.next;
			}
			this.length--;
			return current.element;
		}
		else{
			return null;
		}
	}

	insert(position,element){  //插入
		if(position >-1 && position <= this.length){
			let node = new Node(element);
			let current = this.head;
			let index = 0;
			let previous;
			if(position==0){
				node.next = current;
				this.head = node;
			}else{
				while(index++<position){
					previous = current;
					current = current.next;
				}
				previous.next = node;
				node.next = current;
			}
			this.length++;
			return true;
		}else{
			return false;
		}
	}

	toString(){   //转成字符串
		let current = this.head;
		let str = '';
		while(current){
			str += ','+current.element;
			current = current.next;
		}
		return str;
	}

	indexOf(element){  //索引
		let current = this.head;
		let index = 0;
		while(current){
			if(current.element == element){
				return index;
			}
			index++;
			current = current.next;
		}
		return -1;
	}

	reverse(){   //反转
		if(this.head === null  || this.head.next===null) return;
		let current = this.head;
		let pnext = current.next;
		current.next = null;
		while(pnext){
			let pp = pnext.next;
			pnext.next = current;
			current = pnext;
			pnext = pp;
		}
		this.head = current;
	}

}

let link = new LinkedList();
link.append("111");
link.append("222");
link.append("333");
link.reverse();
console.log(link);
console.log(link.indexOf("111"));

反转结果:

  1. 检查链表中是否有循环
  2. 返回链表倒数第n个元素
  3. 移除链表中的重复元素

解释:图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在图中的数据元素,我们称之为顶点(Vertex),顶点集合有穷非空。在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。

  1. 判断图是否为树:确保图是连通的,不含环的图

    (1)是否有环:要两个数组,一个二维数组作为图的邻接矩阵,一个一维数组标记某个节点是否遍历过 (2)是否连通:检查上面的一维数组是否有遍历到

  2. 统计图中边的个数,n节点:完全有向图n(n-1),完全无向图n(n-1)/2

N叉树、平衡树、二叉树、二叉查找树、平衡二叉树、红黑树、B树

二叉树(节点分支<=2)

总结:

  1. n个节点的二叉树,分支树为n-1
  2. 若二叉树的高度为h,则最少有h个节点,最多2^h -1个节点(满二叉树)
  3. 含有n个节点的二叉树,高度最大n,高度最小log2(n+1)向上取整
  4. 具有n个节点的完全二叉树,高度为log2(n+1)向上取整
  5. 哈夫曼树:权值最小的二叉树

平衡二叉树

非叶子节点最多两个子节点;左子节点小于右子节点;左右两边层级相差不大于1;没有相同重复节点

红黑树也是一种平衡二叉树

  • 哈希表

排序算法

排序算法,这里主要详细介绍四种,描述笔者切身理解,日后会继续叠加其他内容

算法复杂度

怎么定义一种排序算法稳定不稳定?

(1)稳定:排序前a在b前,a=b,排序后a仍在b前(冒泡插入归并基数) (2)不稳定:排序前a在b前,a=b,排序后a可能在b后(选择快速希尔

下面从小到大排序依次分析各种实现:

  • 冒泡排序

从数组中第一个数开始,依次遍历数组中的每一个数,通过相邻比较交换,每一轮循环下来找出剩余未排序数的中的最大数并”冒泡”至数组的最后一个。

//冒泡
for(i=0;i<len-1;i++){
	for(j=0;j<len-1-i;j++){//每一轮最后一个元素都是最值,所以可以不用再比
		if(arr[j]>arr[j+1]){
			var temp = arr[j];
			arr[j] = arr[j+1];
			arr[j+1] = temp;
		}
	}
}
  • 选择排序

从所有记录中选出最小的一个数据元素与第一个位置的记录交换;然后在剩下的记录当中再找最小的与第二个位置的记录交换,循环到只剩下最后一个数据元素为止。

//选择
for(i=0;i<len-1;i++){
	var minIndex = i;
	for(j=i+1;j<len;j++){
		if(arr[j]<arr[minIndex]){
			minIndex = j;
		}
	}
	var temp = arr[i];
	arr[i] = arr[minIndex];
	arr[minIndex] = temp; 
}
  • 插入排序

从待排序的n个记录中的第二个记录开始,依次与前面的记录比较并寻找插入的位置,每次外循环结束后,将当前的数插入到合适的位置。

//插入
for(i=1;i<len;i++){
	if(arr[i]<arr[i-1]){
		var temp = arr[i];
		var j = i-1;
		while(j>=0 && temp<arr[j]){
			arr[j+1] = arr[j];
			j--;
		}
		arr[j+1] = temp;
	}
}

插入排序优化:即找到要插入的位置时,我们可以用二分查找来找到该位置

//  优化(二分查找)
for(var i = 1;i<len;i++){
	var key = arr[i];
	var j = i-1;
	var right = i-1;
	var left = 0;
	while(left<=right){
		var mid = parseInt((left+right)/2);
		if(key<arr[mid]){
			right = mid-1;
		}
		else{
			left = mid+1;
		}
	}
	// 这里最终找到的是left
	for(var j=i+1;j>=left;j--){
		arr[j+1] = arr[j];
	}
	arr[left] = key;
}
  • 快速排序

从数列中挑出一个元素为基准,另外创建两个数组left和right,把比基准小的放在left中,把比基准大的放在right中,并且依此递归,最终并接两个数组得到的就是排序后的数组。

var quickSort2 = function(arr) {
	if (arr.length <= 1) { return arr; }
	var pivotIndex = Math.floor(arr.length / 2);
	var pivot = arr.splice(pivotIndex, 1)[0];
	var left = [];
	var right = [];
	for (var i = 0; i < arr.length; i++){
		if (arr[i] < pivot) {
			left.push(arr[i]);
		} else {
			right.push(arr[i]);
		}
	}
    return quickSort2(left).concat([pivot], quickSort2(right));
};
  • 希尔排序

  • 归并排序

  • 堆排序

  • 基数排序

里面有些内容还没有补充,持续整理更新。。。有错误请指教,共同进步