是对很久之前的一篇关于用javascript实现常见数据结构的一个整理,其中添加了一些常见的数据结构的应用,很多并未整理,以后会陆续加入
- 2019/11/23 更新:增添了链表的插入排序,链表快慢指针应用,BST最大深度递归实现
- 2019/12/12 更新:增添了用JavaScript构建图类
- 2019/12/12 更新:增添了js图类中图的深拷贝
- 2019/12/13 更新:增添了js图类的更新
- 2019/12/13 更新:增添了js图类的拓扑排序
- 2019/12/14 更新:增添了js图:判断图是DAG还是DCG
- 2019/12/18 更新:增添了Hierholzer算法
- 2020/01/03 更新:修改了链表的插入排序
- 2020/01/31 更新:增添了数据结构堆
- 2020/02/01 更新:增添了堆的应用:topK问题,超级丑数
- 2020/02/03 更新:增添了堆的应用:合并小序列
- 2023/02/02 更新:本文通过 js 语言概述了一些常见的数据结构,其中不乏有遗漏,因此每一部分都贴了更为详尽的文章以供阅读,此后均以链接中的文章为准进行更新。
数据结构 javascript 描述
栈
特点:
- 后入先出
- 只能访问栈顶
实现
function Stack() {
this.dataStore = [];
this.top = 0;
this.push = push;
this.pop = pop;
this.peek = peek;
this.clear = clear;
this.size = length;
}
function push(element) {
this.dataStore[this.top++] = element;
}
function peek() {
return this.dataStore[this.top-1];
}
function pop() {
return this.dataStore[--this.top];
}
function clear() {
this.top = 0;
}
function length() {
return this.top;
}
双栈排序
请编写一个程序,按升序对栈进行排序(即最大元素位于栈顶),要求最多只能使用一个额外的栈存放临时数据,但不得将元素复制到别的数据结构中。请注意这是一个栈,意味着排序过程中你只能访问到第一个元素。
测试样例:
in:[1,2,3,4,5]
out:[5,4,3,2,1]
思路:因为只能使用一个辅助栈,所以每次取出原栈的栈顶a都和辅助栈栈顶b进行比较,如果b>a压回原栈,一直重复这个操作就会得到一个排好序的辅助栈
const sortStack=(stack)=>{
let res=new Stack();
if (stack.size()<=0){
return res;
}
while(stack.size()>0){
let temp=stack.peek();
stack.pop();
// 如果辅助栈的栈顶大于原栈的原栈顶,那么把辅助栈的栈顶压回原栈
// 此时原栈的栈顶变成辅助栈的原栈顶,跳出循环后将原栈的原栈顶压入辅助栈,循环进行比较
// 因此根据思路,这一步一定要在把temp压入辅助栈之前
while(res.size()>0&&res.peek()>temp){
stack.push(res.peek());
res.pop();
}
res.push(temp);
}
// 最终得到的res是排序好的为栈的降序排列,
// 因为每次大的数会重新压回原栈,最后的结果就是变成数组升序,降序栈
// 就是数组的升序,于是压回原栈就是一个降序数组,升序栈
while(res.size()>0){
stack.push(res.peek());
res.pop();
}
return stack;
};
/**
* leetcode-155最小栈
* 首先这个题目看似简单,但其实也有一些奥妙在里面,首先要求在常数时间内找到最小的元素
* 这就意味着时间复杂度为O(1)
* 因此我们应该在push数据的时候就已经做好最小数据的检测,因此利用辅助栈来实现获取最小值,同时辅助栈和原栈同步,
* 只是在push数据的时候需要额外判断push哪一个数。同时因为pop操作的存在,不能使用this.min来定义最小值,故而辅助栈是最好的选择
*/
var MinStack = function() {
this.data=[];
this.temp=[];
};
/**
* @param {number} x
* @return {void}
*/
MinStack.prototype.push = function(x) {
if(this.temp.length<1||this.temp[this.temp.length-1]>x){
this.temp.push(x);
}else{
this.temp.push(this.temp[this.temp.length-1]);
}
this.data.push(x);
};
/**
* @return {void}
*/
MinStack.prototype.pop = function() {
this.data.pop();
this.temp.pop();
};
/**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.data[this.data.length-1];
};
/**
* @return {number}
*/
MinStack.prototype.getMin = function() {
return this.temp[this.temp.length-1];
};
栈递归
其实是一种思路,通过辅助栈的方式实现递归的思想,通过栈中元素的出栈和入栈实现类似递归的过程
- 典型案例:leetcode 20 有效的括号
/**
* 易出错点:没处理好所有的情况
* 如果出现嵌套多层的现象怎么处理,这是一个问题
* 可以用递归来处理这个问题
* 首先真正的用递归去做这个问题在处理“字符串的深度”时出现了重复的判断,因此时间复杂度会进行累积,最坏的情况是1+2+...+n/2复杂度达到了n^2。
* 而这道题的精髓是可以利用辅助栈来实现递归的过程:此时遍历过程只有一次,时间复杂度降为n,
* 空间复杂度也为n/2(增加注释中的限制条件后,最坏情况应该是把n/2个内容压进栈里面)
* 至此 我们学会了一种通过栈来实现递归的方式
*/
const isValid=s=>{
if(s.length<0) return true;
if(s.length%2!==0) return false;
let sArr=s.split('');
let obj={
'(':')',
'{':'}',
'[':']'
};
const isValid0=sArr=>{
if(sArr.length===0) return true;
let pivot=0;
for(let i=0;i<sArr.length;i++){
if(sArr[i]!=="("&&sArr[i]!=="{"&&sArr[i]!=="["){
pivot=i-1;
break;
}
}
if(sArr[pivot+1]!==obj[sArr[pivot]]){
return false;
}else{
sArr.splice(pivot,2);
return isValid0(sArr);
}
};
return isValid0(sArr);
};
const isValid1=s=>{
let temp=[];
// 词典是必须的
let obj={
'(':')',
'{':'}',
'[':']'
};
for(let i=0;i<s.length;i++){
if(!temp.length){
temp.push(s[i]);
}else{
// 如果栈的深度大于字符串长度的1/2,就返回false。因为当出现这种情况的时候,即使后面的全部匹配,栈也不会为空。
if(temp.length>s.length/2) return false;
if(s[i]===obj[temp[temp.length-1]]){
temp.pop();
}else{
temp.push(s[i]);
}
}
}
return !temp.length;
};
栈和队列相互转换
双栈实现队列,双队列实现栈
var MyQueue = function() {
this.data=[];
// 利用指针判断是不是第一个元素
this.last=-1;
// 记录第一个元素
this.peekX=0;
};
/**
* Push element x to the back of queue.
* @param {number} x
* @return {void}
*/
MyQueue.prototype.push = function(x) {
this.data.push(x);
this.last++;
if(this.last===0){
this.peekX=x;
}
};
/**
* Removes the element from in front of queue and returns that element.
* @return {number}
*/
MyQueue.prototype.pop = function() {
let temp=[];
if (this.data.length===1){
this.last--;
return this.data.pop();
}
while(this.data.length>1){
temp.push(this.data.pop());
}
let temp0=this.data.pop();
this.peekX=temp.pop();
this.data.push(this.peekX);
while(temp.length>0){
this.data.push(temp.pop());
}
this.last--;
return temp0;
};
/**
* Get the front element.
* @return {number}
*/
MyQueue.prototype.peek = function() {
return this.peekX;
};
MyQueue.prototype.empty = function() {
return this.data.length<=0;
};
var MyStack = function() {
this.data=[];
// 创建一个指向队尾的指针
// 这种用指针的方式也有些不妥,应该单独用一个变量来维护
// this.last=-1;
this.topX=0;
};
/**
* Push element x onto stack.
* @param {number} x
* @return {void}
*/
MyStack.prototype.push = function(x) {
this.data.push(x);
this.topX=x;
};
/**
* Removes the element on top of the stack and returns that element.
* @return {number}
*/
MyStack.prototype.pop = function() {
// 不应该用数组的原生方法,只能用队列的方法,而是用两个队列去实现一个栈。
//this.data.splice(this.last,1);
let temp0=[];
while(this.data.length>1){
this.topX=this.data.shift();
temp0.push(this.topX);
}
let temp=this.data.shift();
while(temp0.length>0){
this.data.push(temp0.shift());
}
return temp;
};
/**
* Get the top element.
* @return {number}
*/
MyStack.prototype.top = function() {
return this.topX;
};
/**
* Returns whether the stack is empty.
* @return {boolean}
*/
MyStack.prototype.empty = function() {
return this.data.length<=0;
};
堆
references:
概念
更多详情请参考 详解基于堆的算法
堆(Heap)是一个可以被看成近似完全二叉树的数组。树上的每一个结点对应数组的一个元素。除了最底层外,该树是完全充满的,而且是从左到右填充。—— 来自:《算法导论》
注: 必须是完全二叉树(如果二叉树有n层,那么n-1层必须是满二叉树)
堆包括最大堆和最小堆:最大堆的每一个节点(除了根结点)的值不大于其父节点;最小堆的每一个节点(除了根结点)的值不小于其父节点。
实现
堆常见的操作:
- HEAPIFY 建堆:把一个乱序的数组变成堆结构的数组,时间复杂度为 O(n)。
- HEAPPUSH:把一个数值放进已经是堆结构的数组中,并保持堆结构,时间复杂度为 O(logn)。
- HEAPPOP:从最大堆中取出最大值或从最小堆中取出最小值,并将剩余的数组保持堆结构,时间复杂度为 O(log n)。
- HEAPSORT:借由 HEAPFY 建堆和 HEAPPOP 堆数组进行排序,时间复杂度为O(n log n),空间复杂度为 O(1)。
代码参考我的github
// 小根堆的构建过程
// 小根堆构建
function MinHeap(){
this.data=[];
this.build=build;
this.insert=insert;
this.deleting=deleting;
this.print=print;
this.heapSort=heapSort;
}
function insert(val){
this.data.push(val);
let idx=this.data.length-1;
let fatherIdx=Math.floor((idx+1)/2)-1;
// 注意核查的是父节点的索引值
while(fatherIdx>=0){
if(this.data[fatherIdx]>this.data[idx]){
let temp=this.data[idx];
this.data[idx]=this.data[fatherIdx];
this.data[fatherIdx]=temp;
}
idx=fatherIdx;
fatherIdx=Math.floor((idx+1)/2)-1;
}
}
function deleting(){
let val=this.data[0];
if(this.data.length===1){
return this.data.pop();
}
this.data[0]=this.data.pop();
// 重构最小堆
let idx=0,len=this.data.length;
while(idx<len){
let left=idx*2+1,right=idx*2+2;
let select=left;
if(right<len){
select=(this.data[left]>this.data[right])?right:left;
}
if (select<len&&this.data[select]<this.data[idx]){
let temp=this.data[idx];
this.data[idx]=this.data[select];
this.data[select]=temp;
}
idx=select;
}
return val;
}
function build(arr){
for(let i=0;i<arr.length;i++){
this.insert(arr[i]);
}
}
function heapSort(){
let res=[];
while(this.data.length>0){
res.push(this.deleting());
}
return res;
}
function print(){
console.info('data==>',this.data);
}
export default MinHeap;
// var h=new MinHeap();
// h.build([1,5,6,4,3,2]);
// h.deleting();
// h.print();
// console.info(h.heapSort());
堆排序
// 整理版:堆排序代码
// key 1: how to build maxHeap
// 这里还应该区别于在堆中我们的删除元素的操作直接用while循环处理即可
const buildMaxHeap=(arr,i)=>{
let len=arr.length,
left=i*2+1,
right=i*2+2;
if(i>=len||left>=len||right>=len) return;
let select=(arr[left]>arr[right])?left:right;
if(arr[select]>arr[i]){
let temp=arr[i];
arr[i]=arr[select];
arr[select]=temp;
// 只需要对发生改变的进行重建即可
buildMaxHeap(arr,select);
}
return arr;
};
// console.info(buildMaxHeap([3,7,5,4,2,4,3],0));
/**
* 时间复杂度:O(nlogn)
* @param arr
* @returns {[]}
*/
const heapSort=arr=>{
let res=[];
// 为了避免7,4,6,5,1,3,9这种情况产生必须从根部向上遍历
for(let i=Math.floor(arr.length/2)-1;i>=0;i--){
buildMaxHeap(arr,i);
}
console.info('arr===>',arr);
while(arr.length>0){
if(arr.length===1){
res.unshift(arr.pop());
continue;
}
res.unshift(arr[0]);
arr[0]=arr.pop();
// 但是这里为什么不需要从根部遍历了呢,因为此时是能保证不发生改变的一侧肯定是大根堆
buildMaxHeap(arr,0);
}
return res;
};
console.info(heapSort([3,7,5,4,2,4,3]));
console.info(heapSort([7,4,6,5,1,3,9]));
应用
- 堆排序
- 时间复杂度:O(nlogn)
- 稳定性:不稳定
- 优先级队列
- 合并有序小文件
- 高性能定时器
- 赫夫曼编码、图最短路径、最小生成树
- 求topK问题
- 中位数
- 双堆实现求中位数或百分位数
优先级队列–合并k个有序数组
references:
Merge k Sorted Arrays【合并k个有序数组】【优先队列】
demo
[
[1, 3, 5, 7],
[2, 4, 6],
[0, 8, 9, 10, 11]]
return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].
Challenge Do it in O(N log k).其中N是元素的总个数,k是数组的长度(即有k个有序数组)
- 注:有些题解中将时间复杂度标注为
O(nklogk)
和这边是不冲突的,它讲的是k
个长度为n
的有序数组,所以共有nk
个元素。
关于MinHeapObj
详细代码参看:我的GitHub
具体思路如下:
-
首先将K个有序数组的第一个元素insert到minHeap中
-
delete根节点,push进入result数组,同时插入和根节点位于同一个数组的下一个元素
- 若没有了则继续循环delete根节点即可
-
得到排序好的result数组
import Heap from './algorithm/Heap/MinHeapObj';
const mergeK=arr=>{
let h=new Heap(),res=[],count=0;
for(let i=0;i<arr.length;i++){
h.insert({'row':i,'col':0,'val':arr[i][0]},'val');
count+=arr[i].length;
}
while(res.length<count){
let temp=h.deleting('val');
res.push(temp.val);
if(arr[temp.row][temp.col+1]){
h.insert({'row':temp.row,'col':temp.col+1,'val':arr[temp.row][temp.col+1]});
}
}
return res;
};
leetcode-373-查找和最小的k对数字
有时候做题要善于观察,尤其这种前k对,有序这种字眼的题目。
- 将所有情况枚举放入堆中,此时时间复杂度就是
Nlogx
(其中:N是所有可能的对数,x是多少个有序数组)
const kSmallestPairs = (nums1, nums2, k)=>{
let h=new Heap(),res=[];
for(let i=0;i<nums1.length;i++){
for(let j=0;j<nums2.length;j++){
h.insert({'arr':[nums1[i],nums2[j]],'sum':nums1[i]+nums2[j]},'sum');
}
}
while(res.length<k&&h.data.length){
res.push(h.deleting('sum').arr);
}
return res;
};
- 优化版:不需要枚举处所有情况加入到堆中:
/**
* 当我们学习了利用堆来合并k个有序数组之后,我们可以不枚举所有的情况而找到想要的k个组合
* 注意组合为:[
* [nums1[0]+nums2[0],nums1[0]+nums2[1],nums1[0]+nums2[2]],
* [nums1[1]+nums2[0],nums1[1]+nums2[1],nums1[1]+nums2[2]],
* [nums1[2]+nums2[0],nums1[2]+nums2[1],nums1[2]+nums2[2]],
* ]的形式
* @param nums1
* @param nums2
* @param k
* @returns {[]}
*/
const kSmallestPairs=(nums1, nums2, k)=>{
let h=new Heap(),res=[];
if(nums2.length<1) return res;
for(let i=0;i<nums1.length;i++){
// 首先安排进来每个有序数组的第一个元素
h.insert({'i':i,'j':0,'val':nums1[i]+nums2[0]},'val');
}
while(h.data.length&&res.length<k){
let temp=h.deleting('val');
res.push([nums1[temp.i],nums2[temp.j]]);
if(nums2[temp.j+1]){
h.insert({'i':temp.i,'j':temp.j+1,'val':nums1[temp.i]+nums2[temp.j+1]},'val');
}
}
return res;
};
leetcode-378-有序矩阵中第K小的元素
求TOP-K问题
references:
快速选择排序 Quick select 解决Top K 问题
top-k
问题不要求数组有序,如在以上references中提到的那样,对于解决top-k
问题有很多种解法,当然这不作为本期讲的重点内容,可以参考我的另一篇文章TopK问题三种方法总结,而利用堆这种数据结构为何有其值得推荐的部分呢?
多数常规解法如快排(思想)
,二分查找
等都会对数据访问多次,那么就有一个问题,当数组中元素个数非常大时,如:100亿,这时候数据不能全部加载到内存,就要求我们尽可能少的遍历所有数据。针对这种情况,堆这种数据结构就有了其优越性。为了查找Top k大的数,我们可以使用大根堆来存储最大的K个元素。大根堆的堆顶元素就是最大K个数中最小的一个。每次考虑下一个数x时,如果x比堆顶元素小,则不需要改变原来的堆。如果想x比堆顶元素大,那么用x替换堆顶元素, 同时,在替换之后,x可能破坏最小堆的结构,需要调整堆来维持堆的性质。
利用堆的算法只需要扫描所有的数据一次,且不会占用太多内存空间(只需要容纳K个元素的空间),尤其适合处理海量数据的场景。算法的时间复杂度为O(N * logk),这实际上相当于执行了部分堆排序。
扩展:当K仍然很大,导致内存无法容纳K个元素时,我们可以考虑先找最大的K1个元素,然后再找看K1+1到2*K1个元素,如此类推。(其中容量为K1的堆可以完全载入内存)
具体解法
- 首先把数组中的前k个值用来建一个小根堆(不是大根堆)。
- 之后的其他数拿到之后进行判断,如果遇到比小顶堆的堆顶的值大的,将堆顶元素删除,将该元素放入堆中
- 最终的堆会是数组中最大的K个数组成的结构,小根堆的顶部又是结构中的最小数,因此把堆顶的值弹出即可得到Top-K。
import Heap from './algorithm/Heap/MinHeap';
const topK=(arr,k)=>{
let h=new Heap();
for(let i=0;i<k;i++){
h.insert(arr[i]);
}
for(let i=k;i<arr.length;i++){
if (arr[i]>h.data[0]){
h.deleting();
h.insert(arr[i]);
}
}
return h.data[0];
};
- 时间复杂度:
O(NlogK)
其中N
为数组全部长度,K
即为要求的K
(因为堆中元素的数目永远是K
)
javascript中的堆栈
以上对于javascript中的堆栈概念有简单描述,其实就是引用类型数据和基本类型数据的区别,另外js之所以引入堆栈,通常与垃圾回收机制有关,为了使程序运行时占用内存最少。
leetcode-347-前 K 个高频元素-topK问题
用构建大根堆的方法:时间复杂度:O(nlogk),代码参考top-k-frequent-elements
leetcode-313-超级丑数
这道题有两种解题方法,一种是通过直接计算primes的幂乘积结合 dp来做github,另一种则是通过应用最小堆的insert和delete来做,具有一定的代表性,
当然也存在一定的易错点就是:- 要通过while循环切除那些重复的元素
/**
* 用最小堆计算的方式非常独特,每次加入一批基于primes的根节点的倍数,每次取根节点
* @param n
* @param primes
* @returns {*}
*/
const nthSuperUglyNumber1=(n,primes)=>{
let h=new Heap(),uglies=[];
h.build([1]);
n-=1;
while(n){
let temp=h.deleting();
// 避免重复元素的出现
while(h.data.length>0&&temp===h.data[0]){
temp=h.deleting();
}
// uglies.push(temp);
for(let p of primes){
let t=p*temp;
h.insert(t);
}
n-=1;
}
// console.info(uglies,h.data);
return h.deleting();
};
链表
更多详情请看:详解链表相关算法
单向链表
class Node{
constructor(val){
this.element=val;
this.next=null;
}
}
双向链表
特点:头节点指向null,最后一个节点的next指向null
class Node{
constructor(val){
this.element=val;
this.previous=null;
this.next=null;
}
}
循环链表
特点:头节点的next指向本身,这种行为传导到其他节点,最后链表的尾节点指向头节点
class Node{
constructor(val){
this.element=val;
this.next=null;
}
}
class LinkedList{
constructor(){
this.head=new Node('head');
this.head.next=this.head;
}
find(item){
let cur=this.head;
while(cur.next.element!=='head'&&cur.element!==item){
cur=cur.next;
}
return cur;
}
findPrevious(item){
let cur=this.head;
while(cur.next&&cur.next.element!==item){
cur=cur.next;
}
return cur;
}
insert(newElement,item){
const newNode=new Node(newElement);
let cur=this.find(item);
if (cur.next.element === 'head') {
newNode.next=this.head;
}else{
newNode.next=cur.next;
}
cur.next=newNode;
}
deletes(item){
}
}
let my_llist=new LinkedList();
my_llist.insert(1,'head');
console.info(my_llist.find('head'));
my_llist.insert(2,'head');
console.info(my_llist);
链表需要掌握的知识点有三点:
- 创建一个链表
- 链表的排序
- 检查链表中是否有环
链表的创建
/**
* 首先创建一个链表
*/
function Node(ele){
this.element=ele;
this.next=null;
// judge2 use
this.visited=0;
}
function linkList(){
this.head=new Node('head');
this.find=find;
this.findPre=findPre;
this.insert=insert;
this.delete=delete0;
}
function find(element){
var node=this.head;
while(node&&node.element!==element){
node=node.next;
}
return node;
}
function findPre(element){
var node=this.head;
while(node.next&&node.next.element!==element){
node=node.next;
}
return node;
}
function insert(element,item){
var node=this.find(element);
var nNode=new Node(item);
nNode.next=node.next;
node.next=nNode;
}
function delete0(element){
var node=this.find(element);
var preNode=this.findPre(element);
preNode.next=node.next;
}
var llist=new linkList();
llist.head.next=new Node(1);
llist.insert(1,2);
llist.insert(2,3);
llist.insert(3,4);
llist.insert(4,5);
// 此时创建了一个带环的链表
// llist.find(4).next=llist.find(2);
console.info(llist.head);
链表的复制
注意可以用
JSON.parse(JSON.stringify(list))
实现链表的deepCopy
例子可以参考:leetcode-23-合并K个排序链表-我的题解
链表的排序
插入排序
/**
* 插入排序算法:
* 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
* 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
* 重复直到所有输入数据插入完为止。
*/
function ListNode(val) {
this.val = val;
this.next = null;
}
const insertionSortList = head=>{
/**
* 关键1:合理的进行交换,利用递归的好处在于那些不是在头部插入的链表能很好的保留不需要插入数据的部分,而避免混乱
* @param list0
* @param list1
* @returns {*}
*/
const swap=(list0,list1)=>{
if(list0===null||list0.val>list1.val){
list1.next=list0;
return list1;
}
list0.next=swap(list0.next,list1);
return list0;
};
let temp0=head,res=null;
while(temp0)
{
let next=temp0.next;
// 关键点2:每次使用排序好的链表进行检测
res=swap(res,temp0);
temp0=next;
}
return res;
};
// 非递归写法
/**
* 思考可以不用递归的方式来解决链表的插入排序吗?
* 在原链表上不停的断链重新操作
* pivot是基准值,总是寻找基准值的位置 pivot.next=cur.next
* 同时找到x x.next>cur.val 因此cur.next=x.next x.next=cur
* 此时head已经按我们需要进行了修改,返回即可
* @param head 原链表
* @param pivot 基准值
* @param cur 当前需要被对比的值
* @returns {{next}|*}
*/
const swap=(head,pivot,cur)=>{
let temp0=head,temp1=head;
// 处理第一个元素就比cur.val大的情况:即插入到头部的情况
if(temp0.val>cur.val){
while(temp1){
if(temp1.val===pivot){
break;
}
temp1=temp1.next;
}
temp1.next=cur.next;
cur.next=temp0;
return cur;
}
//处理内部情况
while(temp0.next){
let temp2=temp0.next;
if(temp0.next.val>cur.val){
// 寻找pivot所在的位置
while(temp1){
if(temp1.val===pivot){
break;
}
temp1=temp1.next;
}
// 总是在修改原链表
temp1.next=cur.next;
cur.next=temp0.next;
temp0.next=cur;
return head;
}
temp0=temp2;
}
};
/**
* pivot是基准,可以认为是链表中最大的那个元素
* @param llist
* @returns {{next}|*}
*/
const insertionSortList0=llist=>{
if(!llist||!llist.next) return llist;
// 指针temp
let temp0=llist,temp1=llist.next,pivot=llist.val;
while(temp1){
// 提前存储下一个值,防止下面操作后temp1的值改变
let next=temp1.next;
if(temp1.val>=pivot){
pivot=temp1.val;
}else{
temp0=swap(temp0,pivot,temp1);
}
temp1=next;
}
return temp0;
};
快速排序
主体思路:即快排在链表排序中的体现,首先链表它的特征是只能访问自己的值和next,因此不能像数组那样操作。
则选用两个指针,首先指针p负责整个链表的遍历,指针q负责移动pivot,但它最初只是指针的移动,最终会和最初设立的pivot进行交换位置。让pivot位于中间,也就是说仍然小于pivot的在左边,大于pivot的在右边。
let swap=(node1,node2)=>{
let ele=node1.element;
node1.element=node2.element;
node2.element=ele;
};
let sort0=(start,end)=>{
/** 链表是一个只能知道头节点的一类数据结构
* 注定它的排序方式肯定会区别于普通数组的排序,
* 下面借助快排的思路完成对链表的排序
* 因此定义两个指针,p指针负责不停遍历,q指针负责移动位置,最后和基准交换。
*/
let ele=start.element;
let p=start.next;
let q=start;
while(p!==end){
// console.info(p,end);
if (p.element<ele){
q=q.next;
swap(q,p);
}
p=p.next;
}
// 将基准变换到中间位置
swap(start,q);
return q;
};
let sort=(start,end)=>{
if (start!==end){
let temp=sort0(start,end);
sort(start,temp);
sort(temp.next,end);
}
};
//注意要把第二个参数赋值为null
sort(list.head,null);
console.info(list.head);
快慢指针应用
链表中环的检测
检测链表中的环主要有一下三种方法
/**
* 方法1:创建哈希表,会占用较大的空间
* 事件复杂度O(n)
*/
function judge1(list){
var set=new Set();
while(list){
if (set.has(list)){
return list;
}
set.add(list);
list=list.next;
}
return false;
}
console.info(judge1(llist.head));
/**
* 方法2:给节点添加visited标记
* 事件复杂度为O(n)
*/
function judge2(list){
while(list){
if (list.visited){
return list;
}
list.visited=1;
list=list.next;
}
return false;
}
console.info(judge2(llist.head));
/**
* 方法3:快慢指针法,设定快指针fast,慢指针slow
*/
function judge3(list){
if(!head||!head.next) return false;
let fast=head.next;
let slow=head;
while(1){
if(!fast||!fast.next) return false;
if (fast===slow){
return fast;
}
fast=fast.next.next;
slow=slow.next;
}
return false;
}
console.info(judge3(llist.head));
寻找链表的中间位置
const findMid=(list,end)=>{
if(list===end||!list) return;
let fast=list,slow=list;
while(fast!==end&&fast.next!==end){
fast=fast.next.next;
slow=slow.next;
}
return slow;
};
矩阵
本期暂不讲
树
本期主要讲二叉树,且均为完全二叉树,部分适用于非完全二叉树
- 数组转化为树
- 树转化为数组:BFS
- 遍历方式:前序遍历DFS,中序遍历,后序遍历
- 获取树的深度
- 获取树的最大深度
- 获取树的最小深度
树的遍历
二叉树的前序遍历
- 递归版
- 非递归版
function Node(ele){
this.val=ele;
this.left=null;
this.right=null;
}
const buildTree=(arr,i)=>{
if (i>arr.length-1) return;
let node=new Node(arr[i]);
node.left=buildTree(arr,i*2+1);
node.right=buildTree(arr,i*2+2);
return node;
};
// 递归版
const preOrder=(tree)=>{
let res=[];
const dfs=(node)=>{
if(!node){
return;
}else{
res.push(node.val);
}
dfs(node.left);
dfs(node.right);
};
dfs(tree);
return res;
};
// 实现非递归前序遍历
const preOrder0=(tree)=>{
let res=[];
// 主要依靠数据结构栈,压入和弹出
let temp=[];
tree&&temp.push(tree);
while(temp.length>0){
/** 这个地方解决了我的疑惑
* 因为如果用栈,压入树的时候如何保证不断的压入树,先读出的是根节点呢
* 解决方法就是压入后立即读出来
*/
let tempTree=temp.pop();
res.push(tempTree.val);
if (tempTree.right)
temp.push(tempTree.right);
if (tempTree.left){
temp.push(tempTree.left);
}
}
return res;
};
二叉树的层次遍历
/**
* 下面解决广度优先遍历的问题
* 也就是将一棵树还原成数组
* 主体思路是借助队列来实现
* 最后写完代码有没有发现和前序遍历有点点类似
*/
const BFS=tree=>{
let res=[];
let temp=[];
tree&&temp.push(tree);
while(temp.length>0){
let tempTree=temp.shift();
res.push(tempTree.val);
if (tempTree.left){
temp.push(tempTree.left);
}
if(tempTree.right){
temp.push(tempTree.right);
}
}
return res;
};
/**
* 层次遍历的另一种形式:表现为二维数组
* 同样适用于解决非完全二叉树的问题
* 同样思路可以用来检测一棵树的最大深度(同样适用于非完全二叉树)
*/
const levelOrder = (root)=>{
let temp=[],res=[];
root&&temp.push(root);
let i=0;
while(temp.length>0){
let res0=[],temp0=[];
temp.forEach(item=>{
res0.push(item.val);
if (item.left){
temp0.push(item.left);
}
if (item.right){
temp0.push(item.right);
}
});
res.push(res0);
temp=temp0;
}
return res;
};
二叉树的中序遍历
- 递归版
- 非递归版
// 递归版
const midOrder=(tree)=>{
let res=[];
const dfs=node=>{
if (!node) return;
dfs(node.left);
res.push(node.val);
dfs(node.right);
};
dfs(tree);
return res;
};
/**
* 非递归中序遍历
* 按照上面的前序遍历的思路首先应该把整个树压进栈
* 然后参照中序遍历的特点:先左然后根然后右不停的把左树压进去
* @param tree
* @returns {[]}
*/
const midOrder0=tree=>{
// 结果数组
let res=[];
// 暂存栈
let temp=[];
tree&&temp.push(tree);
while(temp.length>0){
while(tree.left){
tree=tree.left;
temp.push(tree);
}
let tempTree=temp.pop();
res.push(tempTree.val);
// 记住这里,如果没有右树会直接进入循环重复上面压栈的行为,如果有子树会再压栈
// 再pop一棵树出来,一直到pop到最后的整棵树,继续压栈
if (tempTree.right){
tree=tempTree.right;
// 这里应该把右树压进来啊!右树还可能有子树
temp.push(tree);
}
}
return res;
};
二叉树的后序遍历
- 递归版
- 非递归版
// 递归版
const postOrder=tree=>{
let res=[];
const dfs=node=>{
if (!node) return;
dfs(node.left);
dfs(node.right);
res.push(node.val);
};
dfs(tree);
return res;
};
/**
* 后序遍历的非递归写法
* 左右根节点的顺序进行遍历
* @param tree
*/
const postOrder0=tree=>{
let res=[];
let res0=[];
let temp=[];
tree&&temp.push(tree);
while(temp.length>0)
{
/**
* 因为temp的压栈顺序是先整个树,但是随即弹出对整个树进行验证
* 所以temp实际压栈顺序还是左右
* 于是res内部的顺序就变成了根右左,反序后就是左右根和后序遍历顺序一样
*/
let tempTree=temp.pop();
res.push(tempTree);
if (tempTree.left){
temp.push(tempTree.left);
}
if (tempTree.right){
temp.push(tempTree.right);
}
// console.info('temp',temp);
}
res.reverse();
res.forEach(item=>res0.push(item.val));
return res0;
};
/**
* 首先用数组构建一个二叉树
*/
function Node(ele){
this.val=ele;
this.left=null;
this.right=null;
}
const buildTree=(arr,i)=>{
if (i>arr.length-1) return;
let node=new Node(arr[i]);
node.left=buildTree(arr,i*2+1);
node.right=buildTree(arr,i*2+2);
return node;
};
let arr0=[1,2,3,4,5,6,7,8];
let tree=buildTree(arr0,0);
console.info(tree);
const preOrder=(tree)=>{
let res=[];
const dfs=(node)=>{
if(!node){
return;
}else{
res.push(node.val);
}
dfs(node.left);
dfs(node.right);
};
dfs(tree);
return res;
};
const midOrder=(tree)=>{
let res=[];
const dfs=node=>{
if (!node) return;
dfs(node.left);
res.push(node.val);
dfs(node.right);
};
dfs(tree);
return res;
};
const postOrder=tree=>{
let res=[];
const dfs=node=>{
if (!node) return;
dfs(node.left);
dfs(node.right);
res.push(node.val);
};
dfs(tree);
return res;
};
// 实现非递归前序遍历
const preOrder0=(tree)=>{
let res=[];
// 主要依靠数据结构栈,压入和弹出
let temp=[];
tree&&temp.push(tree);
while(temp.length>0){
/** 这个地方解决了我的疑惑
* 因为如果用栈,压入树的时候如何保证不断的压入树,先读出的是根节点呢
* 解决方法就是压入后立即读出来
*/
let tempTree=temp.pop();
res.push(tempTree.val);
if (tempTree.right)
temp.push(tempTree.right);
if (tempTree.left){
temp.push(tempTree.left);
}
}
return res;
};
/**
* 非递归中序遍历
* 之前完全错误的思路,
* 按照上面的前序遍历的思路首先应该把整个树压进栈
* 然后参照中序遍历的特点:先左然后根然后右不停的把左树压进去,因此嵌套循环左树,且循环的是入参
* @param tree
* @returns {[]}
*/
const midOrder0=tree=>{
// 结果数组
let res=[];
// 暂存栈
let temp=[];
tree&&temp.push(tree);
while(temp.length>0){
while(tree.left){
tree=tree.left;
temp.push(tree);
}
let tempTree=temp.pop();
res.push(tempTree.val);
// 记住这里,如果没有右树会直接进入循环重复上面压栈的行为,如果有子树会再压栈
// 再pop一棵树出来,一直到pop到最后的整棵树,继续压栈
if (tempTree.right){
// 划重点,应该赋值给tree,而不是tempTree,因为我们要进入循环检测的是tree
tree=tempTree.right;
// 这里应该把右树压进来啊!右树还可能有子树
temp.push(tree);
}
}
return res;
};
/**
* 后序遍历的非递归写法
* 左右根节点的顺序进行遍历
* @param tree
*/
const postOrder0=tree=>{
let res=[];
let res0=[];
let temp=[];
tree&&temp.push(tree);
while(temp.length>0)
{
/**
* 因为temp的压栈顺序是先整个树,但是随即弹出再压入第二个中间栈res
* 所以temp实际压栈顺序还是左右
* 于是res内部的顺序就变成了根右左,反序后就是左右根和后序遍历顺序一样
*/
let tempTree=temp.pop();
res.push(tempTree);
if (tempTree.left){
temp.push(tempTree.left);
}
if (tempTree.right){
temp.push(tempTree.right);
}
// console.info('temp',temp);
}
res.reverse();
res.forEach(item=>res0.push(item.val));
return res0;
};
/**
* 那么三种非递归的思路是不是一样的呢,我们借助后序遍历的思路解决中序遍历
* 答案是不能不能:因为根不是在一侧,它在中间,就不能操作第一次的时候把tempTree压入第二个中间栈res
*/
/**
* 下面解决广度优先遍历的问题
* 也就是将一棵树还原成数组
* 主体思路是借助队列来实现
* 最后写完代码有没有发现和前序遍历有点点类似
*/
const BFS=tree=>{
let res=[];
let temp=[];
tree&&temp.push(tree);
while(temp.length>0){
let tempTree=temp.shift();
res.push(tempTree.val);
if (tempTree.left){
temp.push(tempTree.left);
}
if(tempTree.right){
temp.push(tempTree.right);
}
}
return res;
};
const depth=tree=>{
let res=0;
let temp=[];
while(tree){
res++;
tree=tree.left;
}
return res;
};
二叉树的深度
二叉树的最大深度
/**
* 二叉树的最大深度检测
*/
const maxDepth = (root)=>{
let temp=[];
root&&temp.push(root);
let i=0;
while(temp.length>0){
i++;
let temp0=[];
temp.forEach(item=>{
if (item.left){
temp0.push(item.left);
}
if(item.right){
temp0.push(item.right);
}
});
temp=temp0;
}
return i;
};
/**
* 计算最大深度的递归方法:
* 时间复杂度:T(n)=2T(n/2)+1
* a=2,b=2 logba=1>c=0,===>时间复杂度是O(n)
* @param tree
* @returns {number}
*/
const findMaxDepth=tree=>{
if(!tree) return 0;
return 1+Math.max(findMaxDepth(tree.left),findMaxDepth(tree.right));
};
二叉树的最小深度
const minDepth = (tree)=>{
if(!tree) return 0;
if (!tree.left&&!tree.right) return 1;
let resL=minDepth(tree.left);
let resR=minDepth(tree.right);
if(!tree.left&&tree.right) return 1+resR;
if(!tree.right&&tree.left) return 1+resL;
return 1+Math.min(resL,resR);
};
二叉树变种
路径之和
/**
* 判断二叉树中是否有一条路径,它的总和为指定的一个数
* 其实这个题很容易能够想到通过递归的方式来实现
* 边界条件,输入 输出
*/
const findSum=(tree,sum)=>{
if(!tree.left&&!tree.right){
return tree.val===sum;
}
let resL=false,resR=false;
if (tree.left){
resL=findSum(tree.left,sum-tree.val);
}
if (tree.right){
resR=findSum(tree.right,sum-tree.val);
}
return resL||resR;
};
从前序遍历+中序遍历还原一棵树
/**
* 已知二叉树的前序和中序遍历,还原一棵二叉树
* 从前序遍历中找到根节点,然后在中序遍历中找到左右子树的规模
* 以此递归
* 同样适用于非完全二叉树
*/
const buildTree = (preorder, inorder) => {
if (preorder.length <= 0 || inorder.length <= 0) {
return null;
}
let root = preorder[0];
let node = new TreeNode(root);
let idx = inorder.indexOf(root);
node.left = buildTree(preorder.slice(1, idx + 1), inorder.slice(0, idx));
node.right = buildTree(preorder.slice(idx + 1), inorder.slice(idx + 1));
// console.info(node);
return node;
};
从中序遍历+后序遍历还原一棵树
const buildTree = (inorder, postorder)=>{
if (inorder.length===0||postorder.length===0){
return null;
}
let root=postorder[postorder.length-1];
let idx=inorder.indexOf(root);
let node=new TreeNode(root);
node.left=buildTree(inorder.slice(0,idx),postorder.slice(0,idx));
node.right=buildTree(inorder.slice(idx+1),postorder.slice(idx,postorder.length-1));
return node;
};
图
references:
《数据结构与算法JavaScript描述》
概念
- 图是由边的集合及顶点的集合组成的。
- 定点有权重,称为成本。
- 如果一个图的顶点对是有序的,则可以称之为有向图。
- 如果图是无序的,则称之为无序图或者无向图。
有向图
无序图
- 由指向自身的顶点组成的路径叫做环,环的长度为0;
- 至少有一条路径,并且路径的第一个顶点和最后一个顶点相同,则为一个圈。
- 没有重复的边或者重复顶点的圈是简单圈。
- 除了第一个和最后一个顶点外,路径的其他顶点有重复的圈称为平凡圈。
- 如果两个顶点之间有路径,那么两个顶点之间是强连通的,如果有向图的所有顶点都是强连通的,那么这个有向图也是强连通的。
graph类
/**
* Vertex类保存顶点和边
* @param label
* @constructor
*/
function Vertex(label){
this.label=label;
this.wasVisited=false;
}
//表示图的边的方法叫做邻接表数组
/**
* 图类
* @param v
* @constructor
*/
function Graph(v) {
this.vertices = v;
this.edges = 0;
// 表示图的边的数组
this.adj = new Array(v);
for (var i = 0; i < v; i++) {
this.adj[i] =[];
// this.adj[i].push('');
}
// 表示该顶点是否被遍历过
this.marked = [];
this.edgeTo=[];
for (var i = 0; i < v; i++ ) {
this.marked[i] = false;
}
this.addEdge = addEdge;
this.showGraph = showGraph;
this.dfs=dfs;
this.bfs=bfs;
this.pathTo=pathTo;
this.hasPathTo=hasPathTo;
}
function addEdge(v, w) {
this.adj[v].push(w);
this.adj[w].push(v);
this.edges++;
}
function showGraph() {
for (var i = 0; i < this.vertices; ++i) {
console.info(i + "->");
for (var j = 0; j < this.vertices; ++j) {
if (this.adj[i][j] !== undefined){
console.info(this.adj[i][j] + ' ');
}
}
console.info();
}
}
/**
* 图的dfs和bfs
*/
function dfs(v) {
this.marked[v] = true;
console.info("dfs Visited vertex: " + v);
this.adj[v].forEach(item=>{
if (!this.marked[item]) {
this.dfs(item);
}
});
}
/**
* bfs仍然是借助队列来实现,和BST的遍历类似
*/
function bfs(v){
var queue=[];
this.marked[v]=true;
queue.push(v);
while(queue.length>0) {
var x = queue.shift();
console.info("bfs Visited vertex: " + x);
this.adj[x].forEach(item => {
if (!this.marked[item]) {
this.edgeTo[item]=x;
this.marked[item] = true;
queue.push(item);
}
});
}
}
/**
* 查找一个图中,从一个顶点到另一个顶点的最短路径,借助了层次遍历的特点
*/
function pathTo(v){
var resource=0;
if(!this.hasPathTo(v)){
return null;
}
var path=[];
var i=v;
while(i!==resource){
path.unshift(i);
i=this.edgeTo[i];
}
path.unshift(resource);
console.info(path);
return path;
}
function hasPathTo(v){
return this.marked[v];
}
let g = new Graph(7);
console.info(g);
g.addEdge(0,1);
g.addEdge(0,2);
g.addEdge(1,3);
g.addEdge(3,4);
g.addEdge(2,5);
g.addEdge(5,6);
g.addEdge(6,4);
g.showGraph();
// g.dfs(0);
g.bfs(0);
g.pathTo(4);
更新后的Graph类以及图的dfs bfs
function Graph(points){
this.vertices=points;
// 存储某个顶点与他连通的点的数组
this.adj=new Array(points);
// 初始化
for(let i=0;i<points;i++){
this.adj[i]=[];
}
this.addEdge=addEdge;
}
function addEdge(v,w){
this.adj[v].push(w);
this.adj[w].push(v);
this.vertices++;
}
/**
* 深度优先遍历
* @param graph
* @param start
*/
function DFSearch(graph,start){
let map=new Map();
let res=[];
function dfs(start){
if(!graph.adj[start]) return;
map.set(start,true);
res.push(start);
graph.adj[start].forEach(item=>{
if(!map.has(item)){
dfs(item);
}
});
}
dfs(0);
return res;
}
/**
* 广度优先遍历,借助队列来完成
* @param graph
* @param start
*/
function BFSearch(graph,start){
let res=[],temp=[],map=new Map();
temp.push(start);
while(temp.length>0){
let tempPoint=temp.shift();
res.push(tempPoint);
map.set(tempPoint,true);
graph.adj[tempPoint].forEach(item=>{
if(!map.has(item)){
map.set(item,true);
temp.push(item);
}
})
}
return res;
}
对图进行深拷贝
/**
* 使用bfs,借助map实现对图的深拷贝
* @param node
* @returns {undefined|Node}
*/
const cloneGraph = (node)=>{
let temp=[],res=new Map();
let clone=new Node(node.val,[]);
res.set(node,clone);
node&&temp.push(node);
while(temp.length>0){
let tempNode=temp.shift();
for(let i of tempNode.neighbors){
let px=new Node(i.val,[]);
if (!res.has(i)){
temp.push(i);
res.set(i,px);
}
res.get(tempNode).neighbors.push(res.get(i));
}
}
return clone;
};
/**
* 也可以借助DFS实现对图的深拷贝,无论DFS还是BFS,都会对图中的内容进行一次遍历
*/
const cloneGraph1=node=>{
let res=new Map();
const dfs=node0=>{
if(!node) return;
if(res.has(node0)){
return res.get(node0);
}
let clone=new Node(node0.val,[]);
res.set(node0,clone);
for(let nei of node0.neighbors){
clone.neighbors.push(dfs(nei));
}
return clone;
};
return dfs(node);
}
寻找从start到end的最短路径
const findMinPath=(graph,start,end)=>{
// 首先进行层次遍历
let temp=[],edgeTo=[],marked=[];
start!==undefined&&temp.push(start);
// 用edgeTo保存邻接点
while(temp.length>0){
let tempPoint=temp.shift();
graph.adj[tempPoint].forEach(item=>{
// 一定要检查这个点是否遍历过,避免出现死循环
if(!marked[item]){
edgeTo[item]=tempPoint;
marked[item]=true;
temp.push(item);
}
})
}
let res=[],i=end;
if(!marked[end]) return null;
while(i!==start){
res.unshift(i);
i=edgeTo[i];
}
res.unshift(start);
console.info(edgeTo);
return res;
};
console.info(findMinPath(g,0,4));
节点计算过程图:
拓扑排序
references:
拓扑是研究几何图形或空间在连续改变形状后还能保持不变的一些性质的一个学科。它只考虑物体间的位置关系而不考虑它们的形状和大小。
概念
拓扑排序就是对有向图的所有顶点进行排序,使有向图从前面的顶点指向后面的顶点。
所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程
拓扑排序代码
算法思路:
-
从图中选择一个没有前驱的顶点(该顶点的入度为0)并且输出它;
-
从图中删去该顶点,并且删去从该顶点发出的全部有向边;
-
重复上述两步,直到剩余图中不再存在没有前驱的顶点为止。
/**
* 拓扑排序针对的一定是有向图,所以添加边的方式会跟无向图有所区别
* 拓扑排序也可以检测有向图中是否有环
* @param arr
* @param num
*/
const topoSort=(arr,num)=>{
// save the vertex
let adj=new Array(num),queue=[],inDegree=new Array(num).fill(0),res=[];
// 记录遍历的顶点数
let count=0;
// initialize
for(let i=0;i<num;i++){
adj[i]=[];
}
// 统计顶点的入度,即有多少个边指向这个顶点
// 更新邻接表
for(let i=0;i<arr.length;i++){
inDegree[arr[i][1]]+=1;
adj[arr[i][0]].push(arr[i][1]);
}
// 统计入度为0的顶点
// 即从AOV(active on vertex network)上找出没有前驱的顶点并输出它
// 从AOV网中删除这个顶点,并且删去从该顶点发出的所有有向的边(入度-1)
for(let i=0;i<num;i++){
if(inDegree[i]===0){
queue.push(i);
}
}
while(queue.length>0){
// 用队列的shift还是栈的pop区别不大,因为永远都是只有一个元素
let top=queue.shift();
res.push(top);
count++;
for(let i=0;i<adj[top].length;i++){
inDegree[adj[top][i]]-=1;
if(inDegree[adj[top][i]]===0){
queue.push(adj[top][i]);
}
}
}
console.info(res);
return count===num;
};
// test
let arr=[
[0,1],
[1,3],
[1,4],
[1,2],
[2,5]
];
console.info(topoSort(arr,6));
// [0, 1, 3, 4, 2, 5] true
检测有向图中的环
除了拓扑排序的方法还可以用dfs:
/**
* 检查有向图中是否有环检查是DAG(directed acyclic graph) VS DCG(directed cyclic graph)
* dfs与标记「正在访问」检查法
*/
const checkDAG=(arr,num)=>{
let adj=new Array(num),visited=new Array(num).fill(false),
being_visited=new Array(num).fill(false);
// initialize
for(let i=0;i<num;i++){
adj[i]=[];
}
for(let i=0;i<arr.length;i++){
adj[arr[i][0]].push(arr[i][1]);
}
const dfs=(v,being_visited,visited)=>{
// 如果dfs时当前顶点正在被访问,那么必定有环
if(being_visited[v]){
// 输出环所在的位置
console.info('cycle==>',v);
return false;
}
// 顶点并没有正在被访问,但是访问过了,那么略过
if(visited[v]) return true;
being_visited[v]=true;
visited[v]=true;
for(let i=0;i<adj[v].length;i++){
if(!dfs(adj[v][i],being_visited,visited)) return false;
}
// 正常退场
being_visited[v]=false;
return true;
};
// 检查每一个顶点的下个顶点是否有环
for(let i=0;i<num;i++){
if(visited[i]) continue;
if(!dfs(i,being_visited,visited)) return false;
}
return true;
};
Hierholzer算法
references:
问题的起源是leetcode-332-重新安排行程
欧拉迹:一条包含图中所有边的一条路径,该路径中的所有边会且只会出现一次。
一个无向图中包含欧拉迹,当且仅当下面两条性质满足:
- 图是连通的(如果图中任意两点都是连通的,那么图被称作连通图。)
- 图中每个顶点的度都是偶数
一个有向图中包含欧拉迹,当且仅当下面两条性质满足:
- 图是连通的
- 图中每个顶点的入度和出度相同;或者是有一个点出度比入度多1,另有一个点入度比出度多1;
Hierholzer算法
// 原理就是:从一个可能的起点出发,进行深度优先搜索
// 每次沿着辅助边从某个顶点移动到另外一个顶点的时候,都需要删除这个辅助边
// 也就是从邻接表中删除掉这个顶点
// 如果邻接表为空了,那么将所在结点加入到结果列表中去。
const dfs=temp=>{
while(adj.get(temp)&&adj.get(temp).length>0){
dfs(adj.get(temp).shift())
}
res.unshift(temp);
};
性质
- 性质1. 如果图中包含闭欧拉迹,则栈的底部存储的必定是起点。如果图中包含的是开欧拉轨迹,则栈底部存储的是与起点不同的另外一个奇度数端点。
证明:当我们要入栈时,说明当前所在顶点没有任何边了。考虑到从起点出发到当前结点的路径中,除了起点和当前顶点外,所有的顶点都失去了偶数度数(在移除了途经的边后)。如果起点和当前顶点不同,那么两者都失去了奇数度数。如果图中包含闭欧拉迹,这意味着所有顶点的初始度数都是偶数,而当前顶点的剩余度数为0,加上失去的奇数度数表示当前顶点的初始度数必定是奇数,当然这是不可能的,因此假设不成立,当前顶点就是起点。同样的对于开欧拉迹,当前顶点不可能是起点,否则起点的度数就是偶数,而开欧拉迹中起点和终点的度数一定是奇数。这样就能推出当前顶点既不是起点度数也是偶数,因此一定是终点。
const findItinerary1 = (tickets)=>{
let adj=new Map(),res=[];
// initialize
for(let i=0;i<tickets.length;i++){
if (!adj.has(tickets[i][0])){
adj.set(tickets[i][0],[tickets[i][1]]);
}else{
adj.get(tickets[i][0]).push(tickets[i][1]);
}
}
adj.forEach((value,key)=>{
adj.get(key).sort();
});
// console.info(adj);
const dfs=temp=>{
console.info('===>',temp,adj);
while(adj.get(temp)&&adj.get(temp).length>0){
dfs(adj.get(temp).shift())
}
res.unshift(temp);
};
dfs('JFK');
return res;
};
console.info(findItinerary1([["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"],["SFO","JFK"]]));
//["JFK", "ATL", "JFK", "SFO", "ATL", "SFO", "JFK"]
注:欧拉闭迹可以认为是能够一笔画出来的图
- 性质2.如果图中包含闭欧拉迹,入栈的倒数第二个顶点一定是路径中的第二个顶点。
证明:由于路径中的第二个顶点入栈,说明起点已经入栈过,换言之,起点已经没有多余的边了。