数据结构与算法
- 数据结构与算法有什么关系?
可以容纳数据的结构称为数据结构。
算法是用来对数据结构进行处理的方法。
数据结构是静态的,算法是动态的。
线性数据结构(一维数据结构)
线性数据结构强调存储与顺序。
数组
数组特性:
1. 存储在物理空间上是连续的。
2. 底层的数组长度是不变的。(数组定长)
3. 数组的变量指向数组第一个元素的位置。(方括号表示存储地址的偏移量【操作系统小知识: 通过偏移查询数据性能最好】)
优点: 查询性能好。指定查询某个位置。
缺点:
1. 因为空间必须连续,若数组比较大,当系统碎片空间较多的时候,容易存不下。
2. 因为数组的长度固定,所以数组的内容难以被添加和删除。
链表
传递链表,必须传递链表的根节点。(都指单链表)
每一个节点都认为自己是根节点。
链表的特性:
1. 空间上不是连续的。
2. 每存放一个值,都要多开销一个引用空间。
优点:
1. 只要内存足够大,就能存的下,不用担心空间碎片的问题。
2. 链表的添加和删除非常的容易。
缺点:
1. 查询的速度慢,(指的查某个位置)。
2. 链表每一个节点都需要创建一个指向next的引用,浪费一些空间。当节点内数据越多时,这部分多开销的内存影响越少。
链表的逆置
function Node(value){
this.value=value;
this.next=null;
}
let node1 = new Node(1),
node2 = new Node(2),
node3 = new Node(3),
node4 = new Node(4),
node5 = new Node(5);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = null;
function reverseLink(root){
if(root.next.next == null){
root.next.next = root;
return root.next;
}else{
let result = reverseLink(root.next);//在操作之前进行递归,建立函数执行栈,让其从后往前运行
root.next.next = root;
root.next = null;//此处置空是为了保证node1.next指向null
return result;//返回结构一直为node5,保持结果返回
}
}
console.log(reverseLink(node1));
双向链表
没有算法
优点:无论给出任何节点,都能对整个链表进行遍历。
缺点:多费一个引用空间,而且构建双向链表比较复杂。
function Node(value){
this.value = value;
this.prev = null;
this.next = null;
}
let node1 = Node(1),
node2 = Node(2),
node3 = Node(3),
node4 = Node(4),
node5 = Node(5);
node1.prev = null;
node1.next = node2;
node2.prev = node1;
node2.next = node3;
node3.prev = node2;
node3.next = node4;
node4.prev = node3;
node4.next = node5;
node5.prev = node4;
node5.next = null;
线性数据结构的遍历
遍历:将一个集合中的每一个元素进行获取并查看
(算法题必须考虑严谨性判断)
递归遍历必须有出口,一般先找递归出口再递归
排序
排序不是比较大小。
排序的本质是比较和交换。
任何一种排序算法都没有优劣之分,只有是否适合的场景。
冒泡排序
选择排序
选择排序,内层循环,每一圈选出一个最大的,然后放在后面
快速排序
//【简单快排】优化后的版本,不是性能最好的版本,便于记忆
function quickSort(arr){
if(arr == null || arr.length == 0) return [];
let lead = arr[0];
let left = [],
right = [];
for(let i = 0;i < arr.length; i++){
if(arr[i] < lead) left.push(arr[i]);
else right.push(arr[i]);
}
left = quickSort(left);
right = quickSort(right);
left.push(lead);
return left.concat(right);
}
//【标椎快排】
function swap(arr, a, b){
arr[a]=[arr[b]][(arr[b] = arr[a], 0)];
}
function quickSort2(arr, begin, end){//begin和end为下标
if(begin >= end -1) return ;
let left = begin,
right = end -1;
do{
//左指针从左向右查找比arr[begin]大的数,如果有则跳出循环进行交换,如果没有则继续向右查找
do{left++;}while(left < right && arr[left] < arr[begin]);
//当左指针跳出循环后,右指针从右向左查找比arr[begin]小的数,如果有则跳出循环,否则继续
do{right--;}while(left > right && arr[right] > arr[begin]);
//左指针找到第一个比arr[begin]大的数,右指针找到第一个比其小的数,相互交换位置
if(left < right) swap(arr, left, right);
}while(left < right);
//一圈循环完毕,左边都比arr[begin]小
let swapPoint = left == right ? right - 1 : right;
swap(arr, begin, swapPoint);//将中间数据和begin位置数据互换
quickSort2(arr, begin, swapPoint);
quickSort2(arr, swapPoint + 1, end);
}
function quickSort(arr){
quickSort(arr, 0, arr.length);
}
栈和队列
栈:先进后出
队列:先进先出
二维数据结构
二维数组
二维拓扑结构(图)
树结构(有向无环图)
树形结构有一个根节点
树形结构没有回路
叶子节点:下边没有其他节点
节点:既不是根节点,也不是叶子节点
子节点:某个节点下面的节点
树的度:这棵树有最多叉的节点有多少个叉,这棵树的度就为多少
树的深度:树最深有几层
二叉树
树的度最多为2的树形结构
满二叉树:
(1) 所有的叶子节点都在最底层
(2) 每个非叶子节点都有两个子节点
完全二叉树:
国内定义:
(1) 叶子节点都在最后一层或倒数第二层
(2) 叶子节点都向左聚拢
国际定义:
(1) 叶子节点都在最后一层或倒数第二层
(2) 如果有叶子节点,就必然有两个叶子节点
在二叉树中,每个节点都认为自己是根节点
子树:二叉树中,每一个节点或叶子节点,都是一颗子树的根节点
左子树、右子树
二叉树的遍历
传递二叉树要传根节点。
前序遍历:(先根次序遍历) 先打印当前的,再打印左边的,再打印右边的
中序遍历:(中根次序遍历) 先打印左边的,再打印当前的,再打印右边的
后序遍历:(后跟次序遍历) 先打印左边的,再打印右边的,再打印当前的
//二叉树结构
function Node(value){
this.value = value;
this.left = null;
this.right = null;
}
let a = new Node("a"),
b = new Node("b"),
c = new Node("c"),
d = new Node("d"),
e = new Node("e"),
f = new Node("f"),
g = new Node("g");
a.left = c;
a.right = b;
c.left = f;
c.right = g;
b.left = d;
b.right = e;
- 前序遍历
//前序遍历
function f1(root){
if(root == null) return;
console.log(root.value);
f1(root.left);
f1(root.right);
}
f1(a);
- 中序遍历
//中序遍历
function f1(root){
if(root == null) return;
f1(root.left);
console.log(root.value);
f1(root.right);
}
- 后序遍历
//后序遍历
function f1(root){
if(root == null) return;
f1(root.left);
f1(root.right);
console.log(root.value);
}
- 给出二叉树,写出前序中序后序的遍历
- 写出前序中序后序遍历的代码
- 给出前序中序还原二叉树,要求写出后序遍历
- 给出后序中序还原二叉树,要求写出前序遍历
- 代码实现前序中序还原二叉树
- 代码实现后序中序还原二叉树
心得:
前序遍历、后序遍历找根节点;
中序遍历找根节点的左右子树或子节点
前序中序还原二叉树代码
var front = ['a', 'c', 'f', 'g', 'b', 'd', 'e'];
var middle = ['f', 'c', 'g', 'a', 'd', 'b', 'e'];
function recover(front, middle){
if(front == null || middle == null || front.length == 0 || middle == 0 || front.length !== middle.length) return null;
let root = new Node(front[0]);
let rootIndex = middle.indexOf(root.value);//找到根节点在中序遍历中的位置
let fl = front.slice(1, rootIndex + 1),//前序遍历的左子树
fr = front.slice(rootIndex + 1),//前序遍历的右子树
ml = middle.slice(0, rootIndex),//中序遍历的左子树
mr = middle.slice(rootIndex + 1);//中序遍历的右子树
root.left = recover(fl, ml);//根据左子树的前序和中序还原左子树并赋值给root.left
root.right = recover(fr, mr);//根据右子树的前序和中序还原右子树并赋值给root.right
return root;
}
var root = recover(front, middle);
console.log(root.left);
console.log(root.right);
后序中序还原二叉树代码
var middle = ['f', 'c', 'g', 'a', 'd', 'b', 'e'];
var behind = ['f', 'g', 'c', 'd', 'e', 'b', 'a'];
function recover(middle, behind){
if(behind == null || middle == null || behind.length ==0 || middle.length == 0 || behind.length !== middle.length) return null;
let root = new Node(behind[behind.length - 1]);
let rootIndex = middle.indexOf(root.value);
let ml = middle.slice(0, rootIndex),
mr = middle.slice(rootIndex + 1),
bl = behind.slice(0, rootIndex),
br = behind.slice(rootIndex, behind.length - 1);
root.left = recover(ml, bl);
root.right = recover(mr, br);
return root;
}
var root = recover(middle, behind);
console.log(root.left);
console.log(root.right);
二叉树的搜索
树的搜索,图的搜索,爬虫的逻辑,搜索引擎的爬虫算法。
深度优先搜索:更适合探索未知
广度优先搜索:更适合探索局域
//深度优先搜索
//对于二叉树来说,深度优先搜索和前序遍历的顺序一样
function deepSearch(root, target){
if(root == null) return false;
if(root.value == target) return true;
let left = deepSearch(root.left, target),
right = deepSearch(root.right, target));
return left || right;
}
//广度优先搜索
function wideSearch(rootList, target){
if(rootList == null || rootList.length == 0) return false;
let children =[];//当前层所有节点的子节点,都在这个List中,这样传入下一层级的时候,就可以遍历整个层级的节点。
for(let i = 0; i < rootList.length; i++){
if(rootList[i] != null && rootList[i].value == target) return true;
else{
children.push(rootList[i].left);
children.push(rootList[i].right);
}
}
return wideSearch(children, target);
}
console.log(wideSearch([a],'f'));
二叉树的比较
遇到二叉树比较时,必须确定该树的 左右子树互换位置后是否仍为同一棵树
如果笔试,无特殊说明,互换后为不同的树;
如果面试,尽量问一下确认。
//原树
var a1 =new Node('a'),
b1 =new Node('b'),
c1 =new Node('c'),
d1 =new Node('d'),
e1 =new Node('e'),
f1 =new Node('f'),
g1 =new Node('g');
a1.left = c1;
a1.right = b1;
c1.left = f1;
c1.right = g1;
b1.left = d1;
b1.right = e1;
//待比较树
var a2 =new Node('a'),
b2 =new Node('b'),
c2 =new Node('c'),
d2 =new Node('d'),
e2 =new Node('e'),
f2 =new Node('f'),
g2 =new Node('g');
a2.left = c2;
a2.right = b2;
c2.left = f2;
c2.right = g2;
b2.left = d2;
b2.right = e2;
function compareTree(root1, root2){
if(root1 == root2) return true;//同一颗树
if(root1 == null && root2 != null || root1 != null && root2 == null) return false;//其中 一个为空,一个不为空
if(root1.value != root2.value) return false;//相同位置的值不等
return compareTree(root1.left, root2.left) && compareTree(root1.right, root2.right)
|| compareTree(root1.left, root2.right) && compareTree(root1.right, root.left);
}
二叉树的diff算法
//新增什么、修改什么、删除什么
//{type: "新增", origin: null, now: c2}
//{type: "修改", origin: c1, now: c2}
//{type: "删除", origin: c2, now: null}
function diffTree(root1, root2, diffList){
if(root1 == root2) return diffList;
if(root1 == null && root2 != null) {//新增了节点
diffList.push({type: "新增", origin: null, now: root2});
}else if(root1 != null && root2 == null){//删除了节点
diffList.push({type: "删除", origin: root1, now: null});
}else if(root1.value != root2.value){//修改了节点,相同位置节点值不同
diffList.push({type: "修改", origin: root1, now: root2});
diffTree(root1.left, root2.left, diffList);//修改后还要继续进行diff
diffTree(root1.right, root2.right, diffList);
}else{
diffTree(root1.left, root2.left, diffList);
diffTree(root1.right, root2.right, diffList);
}
}
var diffList = [];
diffTree(a1, a2, diffList);
console.log(diffList);
图的最小生成树
图的表示法:点集合和边集合
[a, b, c, d, e]
[ //m表示max
0, 4, 7, m, m,
4, 0, 8, 6, m,
7, 8, 0, 5, m,
m, 6, 5, 0, 7,
m, m, m, 7, 0,
]
- 普利姆算法(加点法)
- 任选一个点作为起点
- 找到以当前选中点为起点的路径最短的边
- 如果这个边的另一端没有被连通起来,那么就连接
- 如果这个边的另一端早已连接,则看倒数第二短的边
- 重复2-4直到将所有的点都连通为止。
function Node(value){
this.value = value;
this.neighbor = [];
}
var max = Infinity;
var pointSet = [new Node('A'), new Node('B'), new Node('C'), new Node('D'), new Node('E')];
var distance =[
[0, 4, 7, max, max],
[4, 0, 8, 6, max],
[7, 8, 0, 5, max],
[max, 6, 5, 0, 7],
[max, max, max, 7, 0],
];
/**
* 此方法根据当前已有的节点来进行判断,获取到距离最短的点。
* 需要传入点集合,边集合,当前已经连接进入的集合
*/
function getMinDisNode(allPoints, distance, nowPointSet){
let fromNode = null,//线段的起点
minDisNode = null;//线段的终点
let minDis = Infinity;
//根据当前已有的这些点为起点,依次判断连接其他点的距离是多少
for(let i = 0; i < nowPointSet.length; i++){
let nowPointIndex =pointSet.indexOf(nowPointSet[i]);
for(let j = 0; j < distance[nowPointIndex].length; j++){
let curCompNode = pointSet[j];//当前待比较的节点
if(nowPointSet.indexOf(curCompNode) < 0 //首先这个点不能为已经接入的点
&& distance[nowPointIndex][j] < minDis){ //其次点之间的距离得为目前的最短距离
fromNode = nowPointSet[i];
minDisNode = curCompNode;
minDis = distance[nowPointIndex][j];
}
}
}
fromNode.neighbor.push(minDisNode);
minDisNode.neighbor.push(fromNode);
return minDisNode;
}
function prim(pointSet, distance, start){
let nowPointSet = [];
nowPointSet.push(start);
//获取最小代价的边
while(true){
let minDisNode = getMinDisNode(pointSet, distance, nowPointSet);
nowPointSet.push(minDisNode);
if(nowPointSet.length == pointSet.length) break;
}
}
prim(pointSet, distance, pointSet[2]);
console.log(pointSet);
- 克鲁斯卡尔算法(加边法)
- 选择最短的边进行连接
- 要保证边连接的两端至少有一个点是新的点
- 或者这个边是将两个部落进行连接
- 重复1-3直到将所有的点都连接到一起。
function Node(value){
this.value = value;
this.neighbor = [];
}
var max = Infinity;
var pointSet = [new Node('A'), new Node('B'), new Node('C'), new Node('D'), new Node('E')];
var distance =[
[0, 4, 7, max, max],
[4, 0, 8, 6, max],
[7, 8, 0, 5, max],
[max, 6, 5, 0, 7],
[max, max, max, 7, 0],
];
function canLink(resultList, tempBegin, tempEnd){
let beginIn = null,
endIn = null;
for(let i = 0; i < resultList.length; i++){
if(resultList[i].indexOf(tempBegin) > -1){
beginIn = resultList[i];
}
if(resultList[i].indexOf(tempEnd) > -1){
endIn = resultList[i];
}
}
//两个点都是新的点,(都不在任何部落)——可以连接,产生新的部落
//begin(end)在A部落,end(begin)没有在部落——A部落扩张一个村庄
//begin在A部落,end在B部落——将AB两个部落合并
//begin和end在同一个部落——不可以连接
if(beginIn != null && endIn != null && beginIn == endIn){
return false;
}
return true;
}
function link(resultList, tempBegin, tempEnd){
let beginIn = null,
endIn = null;
for(let i = 0; i < resultList.length; i++){
if(resultList[i].indexOf(tempBegin) > -1){
beginIn = resultList[i];
}
if(resultList[i].indexOf(tempEnd) > -1){
endIn = resultList[i];
}
}
if(beginIn == null && endIn == null){//两个点都是新的点,(都不在任何部落)——可以连接,产生新的部落
resultList.push([tempBegin, tempEnd]);
}else if(beginIn != null && endIn == null){//begin在A部落,end没有在部落——A部落扩张一个村庄
beginIn.push(tempEnd);
}else if(beginIn == null && endIn != null){//end在A部落,begin没有在部落——A部落扩张一个村庄
endIn.push(tempBegin);
}else if(beginIn != null && endIn != null && beginIn != endIn){//begin在A部落,end在B部落——将AB两个部落合并
resultList[resultList.indexOf(beginIn)] = beginIn.concat(endIn);
resultList.splice(resultList.indexOf(endIn),1);
}
tempBegin.neighbor.push(tempEnd);
tempEnd.neighbor.push(tempBegin);
}
function kruskal(pointSet, distance){
let minDis = Infinity,
begin = null,
end = null;
let resultList = []; // 二维数组,代表有多少个“部落”
while(true){
minDis = Infinity;
for(let i = 0; i < distance.length; i++){
for(let j = 0; j < distance[i].length; j++){
let tempBegin = pointSet[i],
tempEnd = pointSet[j];
if(i != j //去掉自己本身的距离,为0
&& distance[i][j] < minDis
&& canLink(resultList, tempBegin, tempEnd)){
minDis = distance[i][j];
begin = tempBegin;
end = tempEnd;
}
}
}
link(resultList, begin, end);
if(resultList.length == 1 //只存在一个“部落”
&& resultList[0].length == pointSet.length)break;
}
}
kruskal(pointSet, distance);
console.log(pointSet);