下面的内容针对每种数据结构详细介绍,针对每种数据结构都会列出常遇到的经典问题和实现方法,主要是从JS角度实现,不过只要思路明白,至于到底用什么语言,在本文中并不是那么重要了
数据结构
-
数组
对于数组,是吾里编程人最熟悉的数据结构了,还记得学生时代经常拿数组和链表比较
说的最多的就是两点:查询和插入删除。当你需要高频做插入删除时,选择链表;需要高频查询时,选择数组。
- 数组插入删除:插入删除一项,首先需要找到满足具体条件的位置,然后当你插入删除一项时需要移动其他所有项,这样才能空出或填满插入删除的位置,这样就动员了所有项,高频的做这些操作,性能上远没有链表直接修改next。
- 链表查询:若想要查询某个具体位置的节点,需要从头节点依次遍历;而数组有下标,可以直接访问该下标的某项。
常见的数组问题
- 查找数组中第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)直接用两层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));
- 合并两个排序好的数组
实现:
对于JS实现很简单,直接concat后sort就好了,如果不用这些方法,可以用两个指针分别指向两个数组,让两个元素进行比较,把小的放到新数组中,并使较小的元素的数组指针加1,继续比较,直到有一个数组遍历完,最后,把另一个数组剩下的元素放到新数组后即可。
- 重新排列数组中的正数和负数
实现:
利用快速排序思想,将整数和负数分别放到right[]和left[]中,然后各自排序,最后concat
-
栈
栈的主要核心:先进后出;栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。
递归函数的实现就利用了栈这种数据结构,当一个递归函数被调用时,被调函数的局部变量、形参的值以及一个返回地址就会储存在递归工作栈中。运行时按照后进先出的顺序,进行函数执行,完成递归操作。
-
使用栈计算后缀表达式
编译原理中,我们利用栈的结构特性实现后缀表达式的计算。
例:中缀表达式a + b * c + ( d * e + f ) * g,转化为后缀表达式之后是a b c * + d e * f + g * +
具体的转换过程:
1)如果遇到操作数,直接将其输出
2)如果遇到操作符,则将其放入栈中,遇到左括号也将其放入栈中
3)如果遇到一个右括号,则将栈元素弹出,将弹出的操作符输出直到遇到左括号为止,左括号只弹出不输出
4)遇到其他的操作符例如 + ,* , (从栈中弹出元素直到遇到发现更低优先级的元素或者栈空为止。弹出这些元素之后,才能将遇到的操作符压入到栈中,有一点要注意,只有遇到 ) 的情况下才弹出 ( 其他情况下都不会弹出)
5)如果读到了输入的末尾,则将栈中的所有元素依次弹出
-
使用栈为栈中的元素排序
实现:先通过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);
详细排序过程:
- in入栈stack[2,1,5],创建空栈help
- stack弹出pop=5,由于此时help为空,直接in入help栈
- 接着,stack弹出pop=1,由于1<5,直接in入help栈(保证help栈是底-顶:大-小)
- 再,stack弹出pop=2,由于1<2,将help中pop=1直接in入栈stack,然后将2入栈help中
- 继续,stack弹出pop=1,由于2>1,直接in入help栈
- 最后将help全都弹出放到stack中即有序的栈生成。
help栈
-
检查字符串中括号是否匹配正确
这里为了简化,直接就判断()是否匹配,若有需要其他符号,可以增加判断
实现思路:
实现过程中,默认先"("后")",若最开始遇到的事")",则直接跳出,显示"右括号多了"。
- 扫描str
- 遇到"(",入栈stack
- 遇到")",判断stack,若为空,右括号多了,返回false;若不为空,判断top栈顶,若不为左括号,则不匹配,返回false;若为左括号,出栈,继续下一个
- 扫描结束后,判断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连起来的一条链,本文中就简单介绍单链表的结构实现和相关问题的实现
- 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"));
反转结果:
- 检查链表中是否有循环
- 返回链表倒数第n个元素
- 移除链表中的重复元素
-
图
解释:图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在图中的数据元素,我们称之为顶点(Vertex),顶点集合有穷非空。在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
-
判断图是否为树:确保图是连通的,不含环的图
(1)是否有环:要两个数组,一个二维数组作为图的邻接矩阵,一个一维数组标记某个节点是否遍历过 (2)是否连通:检查上面的一维数组是否有遍历到
-
统计图中边的个数,n节点:完全有向图
n(n-1),完全无向图n(n-1)/2
-
树
N叉树、平衡树、二叉树、二叉查找树、平衡二叉树、红黑树、B树
二叉树(节点分支<=2)
总结:
- 有
n个节点的二叉树,分支树为n-1 - 若二叉树的高度为
h,则最少有h个节点,最多2^h -1个节点(满二叉树) - 含有
n个节点的二叉树,高度最大n,高度最小log2(n+1)向上取整 - 具有
n个节点的完全二叉树,高度为log2(n+1)向上取整 - 哈夫曼树:权值最小的二叉树
平衡二叉树
非叶子节点最多两个子节点;左子节点小于右子节点;左右两边层级相差不大于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));
};
-
希尔排序
-
归并排序
-
堆排序
-
基数排序
里面有些内容还没有补充,持续整理更新。。。有错误请指教,共同进步