background

Our daily development work cannot avoid dealing with data. When displaying data, the data structure returned by the interface may not be used directly, and a layer of transformation is required. When saving data, the data structure obtained through the form and the data structure defined by the interface may be inconsistent, which requires a layer of transformation. There are also some requirements of business scenarios, such as logical verification of data. Therefore, it is inevitable to use some common data structures and algorithms. This article focuses on the data structures and algorithms that you might use in your front-end development efforts.

The data structure

The stack

A stack is a special kind of linear table. Its characteristic is that it can only be operated at one end of the table. The operable end is called the top of the stack and the unoperable end is called the bottom of the stack. Stack features: first in, last out.

The principle of

Examples in life: steamer for steamed bread, badminton tube.

implementation

We can use JS to simulate stack functionality. In terms of data storage, you can use arrays to store data, or you can use linked lists to store data. Since an array is the easiest way to implement the stack, it’s an array.

Stack operations include pushing, exiting, clearing, obtaining the top element of the stack, obtaining the size of the stack, and so on.

Class Stack {constructor() {this.items = []; } push(item) {// push this.item.push (item); } pop() {return this.items.pop(); } top() {return this.items[this.kitems.length-1]; } clear() {// clear the stack this.items = []; } size() {// get the stack size return this.item.length; } isEmpty() {// check whether the stack isEmpty return this.items.length === 0; }}Copy the code
application
  1. Determine if the parentheses match

Method I Thought analysis:

  • First, the entire string is traversed from beginning to end;
  • When the character “(” is pushed, the character “)” is out of the stack;
  • Return false if the stack is empty.
  • When the string has been traversed, check whether the stack is empty.

Method two:

  • Declare the variable num to 0 and iterate through the entire string;
  • Num = 1; num = 1; num = 1;
  • Return false if num is already 0 when num is reduced by 1 during traversal;
  • When the string is iterated, determine whether num is 0.
// Method 1: stack

function isPairing(str = ' ') {

    const stack = new Stack();

    for(let i of str) {

        if (i === '(') {

            stack.push(i);

        } else if (i === ') ') {

            if (stack.isEmpty()) {

                return false;

            } else{ stack.pop(); }}}return stack.size() === 0;

}



// Method 2: count

function isPairing(str = ' ') {

    let num = 0;

    for(let i of str) {

        if (i === '(') {

            num++;

        } else if (i === ') ') {

            if (num === 0) {

                return false;

            } else{ num--; }}}return num === 0;

}
Copy the code
  1. Determine if the HTML tags match

Thinking analysis:

  • Declare variables stack, nodes; And iterates through the HTML string, looking for the position of the character “<“;

  • If the position of the character “<” is equal to 0:

    • Continue to try to match the HTML end tag, the match is successful and with the top of the stack tag name, the top of the stack pops; Otherwise, an error is reported.
    • When matching the HTML end tag fails, it tries to match the beginning of the start tag, then iterates through the tag attribute pairs, and finally matches the end of the start tag. After matching, the matched label is pushed to the top of the stack. And build node node number;
  • If the position of the character “<” is greater than 0:

    • Then html.slice(0, pos), creates the text node.
function parseHtml(html = ' ') {

    const startIndex = 0;

    const endIndex = 0;

    // Match the tags 
      
,

, etc.
const startTagOpen = /^<([a-zA-Z\d]+)/; // Match
,

and other closed parts of the tag ">, />"
const startTagClose = /^\s*(\/?) >/; // Match attributes const attribute = /^\s*([\w-]+)(? : ([= "^"] *) ")? \s*/; ,

const endTag = /^<\/([a-zA-Z\d]+)>/; const stack = []; const nodes = []; while(html) { // Find the starting position of < const index = html.indexOf('<'); if (index === 0) { ,

let endTagMatch = html.match(endTag); if (endTagMatch) { if (stack[stack.length - 1]) { if (stack[stack.length - 1].tag === endTagMatch[1]) { / / out of the stack stack.pop(); advanced(endTagMatch[0].length); continue; } else { throw new Error('Start tag and end tag do not match: start tag (${stack[stack.length - 1].tag}), the closing tag (${endTagMatch[0]}) `); }}else { throw new Error(`${endTagMatch[0]}There is no start tag '); }}// Then match the beginning of the start tag, such as

,>
let startTagOpenMatch = html.match(startTagOpen); if (startTagOpenMatch) { const node = { nodeType: 1.tag: startTagOpenMatch[1].attrs: [].children: [],}; advanced(startTagOpenMatch[0].length); let end, attr; // Match the tag attribute list while(! (end = html.match(startTagClose)) && (attr = html.match(attribute))) { advanced(attr[0].length); node.attrs.push({ name: attr[1].value: attr[2]}); }// Match the end of the start tag if (end) { if (stack.length === 0) { nodes.push(node); } else { stack[stack.length - 1].children.push(node); } // The closure and tags are not added to the stack if (end[1]! = ='/') { stack.push(node); } advanced(end[0].length); }}}else { / / text const node = { nodeType: 3.textContent: html.slice(0, index) }; if (stack.length === 0) { nodes.push(node); } else { stack[stack.length - 1].children.push(node); } advanced(node.textContent.length); }}function advanced(n) { html = html.substring(n); } return nodes; } parseHtml('<div id="test" class="a b"></div>'); parseHtml('<div id="test" class="a b">Hello World</div>'); parseHtml(
Hello World
); parseHtml('<div id="test" class="a b"><br class="br" />Hello World</div>'); parseHtml('</div>'); parseHtml('<div></p>'); Copy the code
  1. Version number comparison size

Thinking analysis:

  • The version numbers v1 and V2 are divided into arrays according to the symbol “.”, and the size is compared from left to right.
  • V1 is greater than v2 returns 1, v2 is less than v2 returns negative 1, v1 is equal to v2 returns 0.
/* Compare version number size v1: the first version number v2: the second version number returns 0 if the versions are equal, * returns -1 if the first version number is lower than the second, otherwise returns 1. */

function compareVersion(v1, v2) {

    if(! v1 && ! v2)return 0;

    if(! v1)return -1;

    if(! v2)return 1;

    const v1Stack = v1.split('. ');

    const v2Stack = v2.split('. ');

    const maxLen = Math.max(v1Stack.length, v2Stack.length);

    for(let i = 0; i < maxLen; i++) {

        // Must be rounded, otherwise compare sizes in character order

        const prevVal = ~~v1Stack[i];

        const currVal = ~~v2Stack[i];

        if (prevVal > currVal) {

            return 1;

        }

        if (prevVal < currVal) {

            return -1; }}return 0;

}

console.log(compareVersion("2.2.1"."2.2.01")); / / 0

console.log(compareVersion("2.2.1"."2.2.0")); / / 1

console.log(compareVersion("2.2.1"."2.1.9")); / / 1

console.log(compareVersion("2.2".2.1.1 "")); / / 1

console.log(compareVersion("2.2"."2.2.1")); // -1

console.log(compareVersion("2.2.3.4.5.6"."2.2.2.4.5.12")); / / 1

console.log(compareVersion("2.2.3.4.5.6"."2.2.3.4.5.12")); // -1
Copy the code
use
  • Vue template compilation converts a template string to an AST.
  • Automatically updates the latest version of the NPM package.
  • Function execution context stack.

The queue

Queues are also a special type of linear table, which can only be deleted at one end of the table and inserted at another point. The end that can be deleted is called the head of the queue and the end that can be inserted is called the tail of the queue. Deleting an element is called dequeuing, and inserting an element is called enqueuing. Like a stack, a queue is a linear table with limited operations. Features of the queue: First-in -First-Out (FIFO).

The principle of

Life example: waiting in line to buy something.

implementation

We can also use JS to simulate the functions of queues. In terms of data storage, you can use arrays to store data, or you can use linked lists to store data. Since arrays are the easiest way to implement queues, we use arrays to implement queues.

Queue operations include queue joining, queue dequeuing, queue clearing, queue header element obtaining, queue length obtaining, and so on.

class Queue {

  constructor() {

    // Store data

    this.items = [];

  }

  enqueue(item) {

    / / team

    this.items.push(item);

  }

  dequeue() {

    / / out of the team

    return this.items.shift();

  }

  head() {

    // Get the first element of the team

    return this.items[0];

  }

  tail() {

    // Get the element at the end of the queue

    return this.items[this.items.length - 1];

  }

  clear() {

    // Clear the queue

    this.items = [];

  }

  size() {

    // Get the queue length

    return this.items.length;

  }

  isEmpty() {

    // Check whether the queue is empty

    return this.items.length === 0; }}Copy the code
application
  1. Joseph’s ring problem

There is an array stored 100 data 0 to 99, requires every two numbers to delete a number, to the end of the loop to the beginning of the continue, to find the last number to be deleted.

Thought analysis

  • Create a queue, enlisting numbers 0 through 99;
  • Circular queue, the numbers in the queue are successively dequeued, and the current numbers in the queue are counted index + 1;
  • Index % 3 = 0; if index % 3 = 0;
  • Until the length of the queue is 1, exit the loop and return the number in the queue.
function ring(arr) {

    const queue = new Queue();

    arr.forEach(v= > queue.enqueue(v));



    let index = 0;

    while(queue.size() > 1) {

        const item = queue.dequeue();

        if (++index % 3! = =0) { queue.enqueue(item); }}return queue.head();

}
Copy the code
  1. Fibonacci series

Fibonacci sequence, also known as golden section sequence, was introduced by mathematician Leonardoda Fibonacci by taking rabbit reproduction as an example, so it is also called “rabbit sequence”, which refers to a sequence like this: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34… Mathematically, the Fibonacci sequence is defined recursively as follows: F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2) (n ≥ 2, n ∈ n *).

function fiboSequence(num) {

    if (num < 2) return num;

    const queue = [];

    queue.push(0);

    queue.push(1);

    for(let i = 2; i < num; i++) {

        const len = queue.length;

        queue.push(queue[len - 2] + queue[len  - 1]);

    }

    return queue;

}
Copy the code
  1. Print Yang Hui triangle

Thinking analysis:

  • It is observed that each row of data in the triangle depends on the data in the previous row;
  • We first create a queue to store data in each row for use in the next row.
  • Then initialize the first line of data 1 into the queue. Here we need two for loops nested. The outer for loop determines the final number of rows printed, and the inner for loop generates the data for each row.
  • When the data of the current row is generated, the data sources in the queue are dequeued one by one, and then the newly generated data is enqueued. Records the current outbound data for generating new data.
function printYangHui(num) {

    const queue = [];

    // Store the first row of data

    queue.push(1);

    for(let i = 1; i <= num; i++) {

        let rowArr = [];

        // Fill Spaces

        for(let j = 0; j < Math.floor((num - i) / 2); j++) {

            rowArr.push(' ');

        }

        let prev = 0;

        for(let j = 0; j < i; j++) {

            const num = queue.shift();

            queue.push(prev + num);

            rowArr.push(num);

            prev = num;

        }

        queue.push(1);

        console.log(rowArr.join(' '));

    }

}

printYangHui(10);
Copy the code
use
  1. Implementing the Onion model

Perfect the code to achieve output 1, 2, 3.

function createApp(){

  return {

    use(fn){},

    run(){},}}const app = createApp();



app.use((next) = >{

  setTimeout(function(){

    next();

  })

  console.log(new Date(),'1');

})

app.use((next) = >{

  console.log(new Date(),'2');

  next();

})

app.use((next) = >{

  console.log(new Date(),'3');

  next();

})

app.run();
Copy the code
  1. The message queue

The list

A chain list is formed by several node chains, which is called chain storage structure.

The difference between lists and arrays

Both lists and arrays can store multiple data, so what’s the difference between a list and an array?

Arrays require a contiguous memory space to store data, which is relatively high memory requirements. A linked list, on the other hand, does not require a contiguic memory space. A linked list is a series of scattered memory blocks connected together by Pointers.

A linked list is a slightly more complex data structure than an array. There is no good or bad in both, each has its own advantages and disadvantages.

Because of the memory properties, arrays allow for quick element lookup, but insertion and deletion require a large number of elements to be moved. The reason is that adjacent elements are next to each other in memory, so there is no gap between them, so you can’t quickly add elements. And when deleted, there will be a gap in the memory space, the natural need to make up.

classification
  • Singly linked list

  • Two-way linked list

  • Unidirectional circular linked list

  • Two-way circular list

implementation
const Node = function (data) {

    this.data = data;

    this.next = null;

}



const node1 = new Node(1);

const node2 = new Node(2);

const node3 = new Node(3);



node1.next = node2;

node2.next = node3;
Copy the code
application
  1. Circular linked list

Given a linked list, how to determine whether there are rings in the list?

Thinking analysis:

  1. We first create two Pointers, 1 and 2, pointing to the head node of the list. Then start a large loop in which pointer 1 moves down one node at a time, pointer 2 moves down two nodes at a time, and then compare whether the two Pointers point to the same node. If they are the same, the list is determined to have a loop, if they are different, the next loop continues.
  2. For example, in the linked list A->B->C->D->B->C->D, both Pointers initially point to node A. In the first round of the loop, pointer 1 moves to node B, and pointer 2 moves to node C. In the second round of the loop, pointer 1 moves to node C, and pointer 2 moves to node B. In the third cycle, pointer 1 moves to node D, and pointer 2 moves to node D. At this time, the two Pointers point to the same node, and it is determined that the linked list has a loop.
  3. Suppose the distance from the list head node to the loop entry point is D, and the loop length of the list is S. So the loop goes S times, which is simply order N. No additional storage space is used except for the two Pointers, so the space complexity is O (1).
const Node = function (data) {

    this.data = data;

    this.next = null;

}



const nodeA = new Node('A');

const nodeB = new Node('B');

const nodeC = new Node('C');

const nodeD = new Node('D');

const nodeE = new Node('E');

nodeA.next = nodeB;

nodeB.next = nodeC;

nodeC.next = nodeD;

nodeD.next = nodeE;

nodeE.next = nodeC;



function isCircularLinkedList(head) {

    if (head === null || head.next === null) {

        return false;

    }

    let point1 = head;

    let point2 = head;

    do {

        point1 = point1.next;

        point2 = point2.next && point2.next.next;

    } while(point1 && point2 && point1 ! == point2);if (point1 === point2) {

        return true;

    }

    return false;

}

console.log(isCircularLinkedList(nodeA));
Copy the code
  1. Cross linked list

Determine whether two single linked lists intersect and find the first node of intersection.

Thinking analysis:

  1. If two lists without a loop intersect at a node, as shown in the figure above, they all have something in common. So if two lists intersect, then the addresses of the ends of the two lists are the same. When the program is implemented, the two single-linked lists are traversed until the tail node. Determine whether the address of the tail node is equal. The time complexity is O(L1+L2).
  2. How do I find the first intersection? To determine whether the two lists intersect, write down the lengths of the two lists, figure out the difference in length, len, and then let the longer list first traverse len lengths, and then let both lists traverse at the same time, to determine whether they are equal, if they are equal, it is the first node that intersects.
function intersectNode(head1, head2) {

  if (head1 && head2) {

    // Calculate the length of the list

    let len1 = 0, p = head1;

    let len2 = 0, q = head2;

    while(p.next) {

      len1++;

      p = p.next;

    }

    while(q.next) {

      len2++;

      q = q.next;

    }

    if (p === q) {

      // p points to the short chain and q points to the long chain

      let len = 0;

      if (len1 > len2) {

        len = len1 - len2;

        p = head2;

        q = head1;

      } else {

        len = len2 - len1;

        p = head1;

        q = head2;

      }

      while(len > 0) {

        len--;

        q = q.next;

      }

      while(p && q && p ! == q) { p = p.next; q = q.next; }returnp; }}return null;

}



const Node = function (data) {

  this.data = data;

  this.next = null;

}



const nodeA = new Node('A');

const nodeB = new Node('B');

const nodeC = new Node('C');

const node1 = new Node('1');

const node2 = new Node('2');

const node3 = new Node('3');

const nodeD4 = new Node('D4');

const nodeE5 = new Node('E5');

nodeA.next = nodeB;

nodeB.next = nodeC;

nodeC.next = nodeD4;



node1.next = node2;

node2.next = node3;

node3.next = nodeD4;

nodeD4.next = nodeE5;



console.log(intersectNode(nodeA, node1));
Copy the code
  1. Palindrome list

Please determine whether a linked list is a palindrome list.

Thinking analysis:

  1. Go through the list from the beginning, concatenating the data of each list forward and backward, and finally compare the forward and backward strings to see if they are equal. If it is equal, it is a palindromic linked list; Otherwise it’s not.
const Node = function (data) {

  this.data = data;

  this.next = null;

}



const node1 = new Node('A');

const node2 = new Node('B');

const node3 = new Node('C');

const node4 = new Node('C');

const node5 = new Node('B');

const node6 = new Node('A');

node1.next = node2;

node2.next = node3;

node3.next = node4;

node4.next = node5;

node5.next = node6;



const isPalindrome = head= > {

    let a = ' ', b = ' ';

    while(head ! = =null) {

        a = a + head.data;

        b = head.data + b;

        head = head.next;

    }

    return a === b;

}

console.log(isPalindrome(node1));
Copy the code
use
  1. Prototype chain
  2. The scope chain

The tree

A tree is a data structure that consists of a set of n(n>=1) finite nodes with a hierarchical relationship. It’s called a tree because it looks like an upside-down tree, meaning it has roots up and leaves down.

classification
  • Unordered tree: A tree in which the children of any node have no order relation to each other is called an unordered tree, also called a free tree.
  • Ordered tree: A tree in which the children of any node have an ordered relationship is called an ordered tree.
  • Binary tree: A tree with a maximum of two subtrees per node is called a binary tree.
  • Full binary tree: a tree in which all nodes except the leaf node contain two subtrees is called a full binary tree.
  • Complete binary tree: A binary tree in which all the layers except the last layer are full of nodes and the last layer lacks the right continuous nodes is called a complete binary tree (the heap is a complete binary tree).
  • Huffman tree (optimal binary tree) : The binary tree with the shortest weighted path is called a Huffman tree or optimal binary tree.
implementation
// Binary tree implementation

function Node(data) {

    this.data = data;

    this.left = null;

    this.right = null;

}

const nodeA = new Node('A');

const nodeB = new Node('B');

const nodeC = new Node('C');

const nodeD = new Node('D');

const nodeE = new Node('E');

const nodeF = new Node('F');

const nodeG = new Node('G');



nodeA.left = nodeB;

nodeA.right = nodeC;

nodeB.left = nodeD;

nodeB.right = nodeE;

nodeC.left = nodeF;

nodeC.right = nodeG;
Copy the code

In our daily work, we come into contact with the most is the multi-fork tree.

traverse

  • Depth first traversal

    • The first sequence traversal

Preorder traversal (also called root-first traversal) is ABDECFG (root-left-right).

  • In the sequence traversal

In-order traversal (also known as in-root traversal) is DBEAFCG (left-root-right) (only binary trees have in-order traversal).

  • After the sequence traversal

The post-order traversal (also called post-root traversal) is called DEBFGCA (left-right-root).

  • Breadth first traversal

    • Sequence traversal

The sequence traversal is ABCDEFG.

use
  1. Tree flattening (showing OCR recognition results)
  2. Flat array to tree (tag tree)

figure

Graph structure is a kind of nonlinear data structure. There are many examples of Graph in real life, such as transportation network, subway network, etc., which can be abstracted into Graph structure. A nonlinear structure with graph structure more complex than tree structure.

A graph is made up of vertices and edges.

classification
  • Undirected graph

A graph structure in which all the edges are directionless is called undirected.

  • Directed graph

A graph whose edges are directional is called a directed graph.

  • Weighted graph

This graph is a weighted graph if you add a value to the edge to indicate the weight.

  • Connected graph

If any two nodes in a graph can find a path to connect them, the graph is said to be connected.

said

There are two ways to represent graph: adjacency matrix and adjacency list. Different scenes and algorithms may require different graph representations. In general, when the number of nodes is very large, the matrix will be very sparse and the space cost will be large. In this case, the representation of adjacent linked list will occupy less space. If it’s a dense matrix or if you need to quickly determine whether any two nodes are connected by edges or whatever, maybe the adjacencies matrix is more appropriate.

  • Adjacency matrix

  • Adjacency list

traverse
  • Depth first traversal
  • Breadth first traversal
use
  • Commodity classification selection

algorithm

LRU

LRU is the abbreviation of Least Recently Used, that is, the Least Recently Used, is a commonly Used page replacement algorithm, the most Recently unused pages to be eliminated.

The core idea is that “if data has been accessed recently, there is a higher chance it will be accessed in the future.”

The principle of

implementation

Thinking analysis:

  • Choose the appropriate data structure.

    • Hash table: O(1) level of time complexity, suitable for data lookup. However, the elements are not ordered, and there is no way to determine the order in which the elements are accessed.
    • Arrays: Inserts and deletes of elements are O(n).
    • Unidirectional linked list: to delete a node, you need to access the precursor node, which takes O(n) to find the previous traversal.
    • Bidirectional linked list: nodes have a precursor pointer. Deleting and moving nodes are pointer changes, which are all O(1).

Conclusion: Hash table + two-way linked list.

The purpose of using hash table is to quickly access the data stored in the bidirectional linked list, and store the key and node reference of the bidirectional linked list; The purpose of using bidirectional linked list is to quickly move and delete the node location, and store key and corresponding data.

  • Set virtual nodes to facilitate quick access to first and last nodes. No real node is added initially, so you need to point the precursor and successor Pointers to the virtual node to yourself.

  • The implementation of the get method.

  • Implementation of the PUT method.

    • To write new data, check the number of current nodes. If the number of nodes reaches the maximum capacity, the nodes at the bottom of the list need to be deleted, then new nodes need to be created, added to the head of the list, and written to the hash table.
    • Writing to existing data updates the data value and moves the node location to the list header.
function Node(key, value) {

    this.key = key;

    this.value = value;

    this.prev = null;

    this.next = null;

}



class LRUCache {

    constructor(capacity) {

        this.capacity = capacity; / / capacity

        this.hash = {}; / / a hash table

        this.count = 0; // Number of current nodes

        this.virtualNode = new Node(); // Virtual nodes



        // Cross-reference

        this.virtualNode.next = this.virtualNode;

        this.virtualNode.prev = this.virtualNode;

    }

    get(key) {

        const node = this.hash[key];

        if (node) {

                this.moveToHead(node);

                returnnode.value; }}put(key, value) {

        const node = this.hash[key];

        if (node) {

            node.value = value;

            this.moveToHead(node);

        } else {

            if (this.count === this.capacity) {

                this.removeLRUItem();

            }

            const newNode = new Node(key, value);

            this.hash[key] = newNode;

            this.addToHead(newNode);

            this.count++; }}remove(key) {

        const node = this.hash[key];

        if (node) {

            this.removeFromList(node);

            Reflect.deleteProperty(this.hash, key);

            this.count--; }}isEmpty() {

        return this.count === 0;

    }

    moveToHead(node) {

        this.removeFromList(node);

        this.addToHead(node);

    }

    removeFromList(node) {

        const prevNode = node.prev;

        const nextNode = node.next;

        prevNode.next = nextNode;

        nextNode.prev = prevNode;

        node.prev = null;

        node.next = null;

    }

    addToHead(node) {

        const nextNode = this.virtualNode.next;

        this.virtualNode.next = node;

        nextNode.prev = node;

        node.prev = this.virtualNode;

        node.next = nextNode;

    }

    removeLRUItem() {

        const tailNode = this.virtualNode.prev;

        this.remove(tailNode.key); }}const cache = new LRUCache(5);

console.log(cache.isEmpty());

cache.put('A'.'A');

cache.put('B'.'B');

cache.put('C'.'C');

cache.put('D'.'D');

cache.put('E'.'E');

console.log(cache.get('A'));

cache.put('F'.'F');

console.log(cache.get('B'));

console.log(cache.isEmpty());

cache.remove('E');

cache.remove('F');

cache.remove('A');

cache.remove('C');

cache.remove('D');

console.log(cache.isEmpty());

console.log(cache);
Copy the code
use
  • Historical browsing history.
  • Cache elimination policy.

❤️ Thanks for your support

That’s all for this share. Hope it helps you

If you like it, don’t forget to share, like and collect it.

Welcome to pay attention to the public number ELab team receiving good articles ~

We are from Bytedance, and we are the front end department of education under bytedance. We are responsible for the front end development of bytedance education products.

We focus on product quality improvement, development efficiency, creativity and cutting-edge technology and other directions to precipitation and dissemination of professional knowledge and cases, to contribute experience value to the industry. Including but not limited to performance monitoring, component library, multi-terminal technology, Serverless, visual construction, audio and video, artificial intelligence, product design and marketing, etc.

Interested students are welcome to click in the comments section or use the push-code to push to the author’s department 🤪

Bytes to beat school/club recruit delivery link: job.toutiao.com/s/eGrF9qQ