diff 算法实现的几种方法和前端中的应用

diff 算法原理和几种实现方法

diff 是什么

diff 算法就是比较两个数据的差异,例如字符串的差异,对象的差异。

常用于版本管理(git)例如下面的实际案例。

github 上某个 commit,旧代码和新代码之间的不同 diff 展示如下。绿色部分表示添加,红色部分表示删除。这里表示整行的删除和增加。
在这里插入图片描述
这里就表示删除了两个空行。
在这里插入图片描述
上面的案例都是整行更新。还有一些行内的变化,例如某一行中(一个字符串)的某一些字符做了删除和增加。这就需要用到 diff 算法求两个字符串的差异。
在这里插入图片描述
字符串的 diff 可以有很多算法实现,其中最常用的就是 LCS 最长公共子序列算法。

最长公共子序列 LCS 算法

diff 可以使用最长公共子序列 LCS 算法进行处理,时间复杂度为O(n2),本质上是 DP 动态规划算法的扩展。

什么是最长公共子序列呢?

最长公共子串(Longest Common Substring)指的是两个字符串中的最长公共子串,要求子串一定连续。

最长公共子序列(Longest Common Subsequence)指的是两个字符串中的最长公共子序列,不要求子序列连续。
在这里插入图片描述
例如,两个字符串 abcde 和 ace 求最长公共子序列,那么实际上就是下面的图示(对应代码在后面)。
在这里插入图片描述
其核心的递推式如下:

二维矩阵中,某一个格点的值可以递推得到:

令两个字符串是 str1 str2

1、如果 str1[i - 1] 等于 str2[j - 1],就是相同的字符,那么当前值就是左上角值 + 1,即 D[i][j] = D[i - 1][j - 1] + 1。对应图中绿色的箭头

2、如果 str1[i - 1] 不等于 str2[j - 1],就是不同的字符,那么当前的值就是上方和左方的值的最大值,即 D[i][j] = Max(D[i - 1][j], D[i][j - 1])。
在这里插入图片描述

通过分析可知,长度是 N 的字符串,计算矩阵的权重,算法需要 O(n ^ 2)的时间复杂度,这个性能不太好。

有些库使用了更精确的查分算法实现。

查分算法

基于“差分算法及其变体”(Myers,1986)中提出的算法。

在这里插入图片描述
这个算法比较复杂

按照我的理解,核心思想如下

感兴趣的朋友可以查找原文阅读 http://www.xmailserver.org/diff2.pdf
在这里插入图片描述

Broadly, jsdiff’s diff functions all take an old text and a new text and perform three steps:

Split both texts into arrays of “tokens”. What constitutes a token varies; in diffChars, each character is a token, while in diffLines, each line is a token.

Find the smallest set of single-token insertions and deletions needed to transform the first array of tokens into the second.

This step depends upon having some notion of a token from the old array being “equal” to one from the new array, and this notion of equality affects the results. Usually two tokens are equal if === considers them equal, but some of the diff functions use an alternative notion of equality or have options to configure it. For instance, by default diffChars(“Foo”, “FOOD”) will require two deletions (o, o) and three insertions (O, O, D), but diffChars(“Foo”, “FOOD”, {ignoreCase: true}) will require just one insertion (of a D), since ignoreCase causes o and O to be considered equal.

Return an array representing the transformation computed in the previous step as a series of change objects. The array is ordered from the start of the input to the end, and each change object represents inserting one or more tokens, deleting one or more tokens, or keeping one or more tokens.

diff 函数都接受一个旧字符串和一个新字符串,并执行三个步骤:

1、将两个文本拆分为“标记”数组,标记的构成各不相同;在diffChar中,每个字符都是一个标记,而在diffLines中,每一行都是标记。

2、找到将第一个 token 数组转换为第二个 token 数组所需的最小单 token 插入和删除集合。

这一步取决于让旧数组中的令牌与新数组中的标记“相等”,而这种相等的概念会影响结果。

通常,如果===认为两个标记相等,则这两个标记是相等的,但一些diff函数使用了另一种相等概念或有配置它的选项。例如,默认情况下,diffChar(“Foo”,“FOOD”)将需要两次删除(o,o)和三次插入(o,o,D),但diffChar(”Foo“,”FOOD“,{ignoreCase:true})将只需要一次插入(D),因为ignoreCase会导致 o 和 O 被视为相等。

3、返回一个数组,将上一步中计算的变换表示为一系列变化对象。数组从输入的开始到结束排序,每个更改对象表示插入一个或多个token、删除一个或更多个token或保留一个或更多个token。

其他改进的 diff 算法

查阅资料,基于上述基本的 diff 算法,很多学者提出了“最长公共子数组”算法,或者优化了虚拟 DOM 的 diff 算法。他们的思想也值得学习,限于篇幅不进行展开。感兴趣的朋友可以查找原始论文阅读。
在这里插入图片描述
说了很多理论知识,那么具体怎么在项目中使用呢?

下面就是若干具体使用的案例。

js 第三方库实现 diff

jsdiff 这个第三方库集成了 diff 算法,使用人数很多,项目中可以放心使用。

可以在 nodejs 实现,也可以在浏览器中实现

在 nodejs 中的案例如下

// jsdiff demo, reference: https://github.com/kpdecker/jsdiff
// nodejs
// "colors": "^1.4.0",
// "diff": "^7.0.0"

require('colors');

const Diff = require('diff');

// 核心:比较两个字符串的差异 Diff.diffChars(s1, s2)
const diffList = Diff.diffChars('Hello Michael Blog', 'Hello Tom Blog');

// 返回值是一个列表,然后循环用不同颜色展示
diffList.forEach((part) => {
  let text = part.value;
  // green for additions
  if (part.added) {
    text = part.value.bgGreen;
  }
  // red for deletions
  if (part.removed) {
    text = part.value.bgRed;
  }
  process.stderr.write(text);
});

react 第三方库实现 diff-viewer

diff-viewer 是 diff 库在 react 框架中进一步的应用

// react-diff-viewer demo, reference: https://github.com/praneshr/react-diff-viewer
import React, { PureComponent } from 'react';
import ReactDiffViewer from 'react-diff-viewer';

// 两端代码(字符串)
const oldCode = `
const a = 10
const b = 10
const c = () => console.log('foo')

if(a > 10) {
  console.log('bar')
}

console.log('done')
`;

const newCode = `
const a = 10
const boo = 10

if(a === 10) {
  console.log('bar')
}
`;

// 直接把代码传入组件中,即可展示。其他的属性和配置详见官方网站。
class Diff extends PureComponent {
  render = () => {
    return (
      <ReactDiffViewer oldValue={oldCode} newValue={newCode} splitView={true} />
    );
  };
}

export default Diff;

python 库实现 diff

python 内置函数也实现了这个效果

import difflib

# https://blog.csdn.net/molangmolang/article/details/138093020

text1 = '一次性执行
JS
PY
TS
的单元测试'
text2 = '一次性执行
javascript
python
typescript
的单元测试'

# 创建Differ对象
differ = difflib.Differ()

# 使用Differ对象生成差异报告
# diff = differ.compare(text1.splitlines(' '), text2.splitlines(' '))
diff = differ.compare(text1.splitlines(keepends=True), text2.splitlines(keepends=True))

# 打印差异报告
print(''.join(diff))

手写 diff 算法实现

很多场景我们不能使用第三方库(管理层觉得第三方库太大,不用;或者管理层觉得维护很慢反,不用;或者自己写作为成果),所以可以自己简单实现一下这部分算法

diff-text.js 实现字符串的 diff

import ObjectUtils from './object-utils';

// 字符串的 diff 关键是分词——按照空格回车换行符等,对字符串进行分词
// 这里不同的算法给出的分词可能不一样,看实际需求是单词还是汉字等
const extendedWordChars = 'a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}\u{4e00}-\u{9fa5}';
const tokenText = new RegExp(`[${extendedWordChars}]+|\s+|[^${extendedWordChars}]`, 'ug');

const buildValues = (diff, components, newString, oldString, valueType, useLongestToken) => {
  let componentPos = 0;
  let componentLen = components.length;
  let newPos = 0;
  let oldPos = 0;

  for (; componentPos < componentLen; componentPos++) {
    let component = components[componentPos];
    if (!component.removed) {
      if (!component.added && useLongestToken) {
        let value = newString.slice(newPos, newPos + component.count);
        // eslint-disable-next-line no-loop-func
        value = value.map((value, i) => {
          let oldValue = oldString[oldPos + i];
          return oldValue.length > value.length ? oldValue : value;
        });

        component.value = diff.join(value, valueType);
      } else {
        component.value = diff.join(newString.slice(newPos, newPos + component.count), valueType);
      }
      newPos += component.count;

      // Common case
      if (!component.added) {
        oldPos += component.count;
      }
    } else {
      component.value = diff.join(oldString.slice(oldPos, oldPos + component.count), valueType);
      oldPos += component.count;

      // Reverse add and remove so removes are output first to match common convention
      // The diffing algorithm is tied to add then remove output and this is the simplest
      // route to get the desired output with minimal overhead.
      if (componentPos && components[componentPos - 1].added) {
        let tmp = components[componentPos - 1];
        components[componentPos - 1] = components[componentPos];
        components[componentPos] = tmp;
      }
    }
  }

  // Special case handle for when one terminal is ignored (i.e. whitespace).
  // For this case we merge the terminal into the prior string and drop the change.
  // This is only available for string mode.
  let lastComponent = components[componentLen - 1];
  if (componentLen > 1
      && typeof lastComponent.value === 'string'
      && (lastComponent.added || lastComponent.removed)
      && diff.equals('', lastComponent.value)) {
    components[componentLen - 2].value += lastComponent.value;
    components.pop();
  }

  return components;
};

const clonePath = (path) => {
  return { newPos: path.newPos, components: path.components.slice(0) };
};

class DiffText {

  constructor(oldValue, newValue, options = {}) {

    this.oldValue = oldValue;
    this.newValue = newValue;
    const oldValueType = ObjectUtils.getDataType(oldValue);
    const newValueType = ObjectUtils.getDataType(newValue);
    this.canCompare = true;

    if (oldValueType !== newValueType) {
      this.canCompare = false;
      return;
    }

    this.valueType = newValueType;
    this.callback = options.callback;
    const optionsType = ObjectUtils.getDataType(options);
    if (optionsType === 'function') {
      this.callback = options;
      this.options = {};
    } else {
      this.options = {};
    }
    this.comparePath = 1;

    this.oldValue = this.removeEmpty(this.tokenize(oldValue, oldValueType), oldValueType);
    this.oldLen = this.oldValue.length;
    this.newValue = this.removeEmpty(this.tokenize(newValue, newValueType), newValueType);
    this.newLen = this.newValue.length;

    this.maxEditLength = this.newLen + this.oldLen;
    if (this.options.maxEditLength) {
      this.maxEditLength = Math.min(this.maxEditLength, this.options.maxEditLength);
    }

  }

  done = (value) => {
    if (this.callback) {
      setTimeout(function () {
        this.callback(undefined, value);
      }, 0);
      return true;
    }
    return value;
  }

  // Main worker method. checks all permutations of a given edit length for acceptance.
  execCompareLength = (bestPath) => {
    for (let diagonalPath = -1 * this.comparePath; diagonalPath <= this.comparePath; diagonalPath += 2) {
      let basePath;
      let addPath = bestPath[diagonalPath - 1];
      let removePath = bestPath[diagonalPath + 1];
      let oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
      if (addPath) {
        // No one else is going to attempt to use this value, clear it
        bestPath[diagonalPath - 1] = undefined;
      }

      let canAdd = addPath && addPath.newPos + 1 < this.newLen;
      let canRemove = removePath && 0 <= oldPos && oldPos < this.oldLen;
      if (!canAdd && !canRemove) {
        // If this path is a terminal then prune
        bestPath[diagonalPath] = undefined;
        continue;
      }

      // Select the diagonal that we want to branch from. We select the prior
      // path whose position in the new string is the farthest from the origin
      // and does not pass the bounds of the diff graph
      if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) {
        basePath = clonePath(removePath);
        this.pushComponent(basePath.components, undefined, true);
      } else {
        basePath = addPath; // No need to clone, we've pulled it from the list
        basePath.newPos++;
        this.pushComponent(basePath.components, true, undefined);
      }

      oldPos = this.extractCommon(basePath, this.newValue, this.oldValue, diagonalPath);

      // If we have hit the end of both strings, then we are done
      if (basePath.newPos + 1 >= this.newLen && oldPos + 1 >= this.oldLen) {
        return this.done(buildValues(this, basePath.components, this.newValue, this.oldValue, this.valueType, this.useLongestToken));
      } else {
        // Otherwise track this path as a potential candidate and continue.
        bestPath[diagonalPath] = basePath;
      }
    }

    this.comparePath++;
  }

  exec = (bestPath) => {
    setTimeout(function () {
      if (this.comparePath > this.maxEditLength) {
        return this.callback();
      }

      if (!this.execCompareLength(bestPath)) {
        this.exec(bestPath);
      }
    }, 0);
  }

  pushComponent = (components, added, removed) => {
    let last = components[components.length - 1];
    if (last && last.added === added && last.removed === removed) {
      // We need to clone here as the component clone operation is just
      // as shallow array clone
      components[components.length - 1] = { count: last.count + 1, added: added, removed: removed };
    } else {
      components.push({ count: 1, added: added, removed: removed });
    }
  }

  extractCommon = (basePath, newString, oldString, diagonalPath) => {
    let newLen = newString.length;
    let oldLen = oldString.length;
    let newPos = basePath.newPos;
    let oldPos = newPos - diagonalPath;
    let commonCount = 0;

    while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newString[newPos + 1], oldString[oldPos + 1])) {
      newPos++;
      oldPos++;
      commonCount++;
    }

    if (commonCount) {
      basePath.components.push({ count: commonCount });
    }

    basePath.newPos = newPos;
    return oldPos;
  }

  equals = (left, right) => {
    if (this.options.ignoreCase) {
      left = left.toLowerCase();
      right = right.toLowerCase();
    }

    return left.trim() === right.trim();
  }

  removeEmpty = (array, type) => {
    if (type === 'Array') return array;
    let ret = [];
    for (let i = 0; i < array.length; i++) {
      if (array[i]) {
        ret.push(array[i]);
      }
    }
    return ret;
  }

  tokenize = (value, valueType) => {
    if (valueType === 'Array') {
      return value.slice();
    }

    let parts = value.match(tokenText) || [];
    const tokens = [];
    let prevPart = null;
    parts.forEach(part => {
      if ((/s/).test(part)) {
        if (prevPart == null) {
          tokens.push(part);
        } else {
          tokens.push(tokens.pop() + part);
        }
      } else if ((/s/).test(prevPart)) {
        if (tokens[tokens.length - 1] === prevPart) {
          tokens.push(tokens.pop() + part);
        } else {
          tokens.push(prevPart + part);
        }
      } else {
        tokens.push(part);
      }

      prevPart = part;
    });
    return tokens;
  }

  join = (value, valueType) => {
    if (valueType === 'Array') return value;
    // Tokens being joined here will always have appeared consecutively in the
    // same text, so we can simply strip off the leading whitespace from all the
    // tokens except the first (and except any whitespace-only tokens - but such
    // a token will always be the first and only token anyway) and then join them
    // and the whitespace around words and punctuation will end up correct.
    return value.map((token, i) => {
      if (i === 0) {
        return token;
      } else {
        return token.replace((/^s+/), '');
      }
    }).join('');
  }

  getDiffs = () => {

    if (!this.canCompare) {
      return [
        { value: this.oldValue, removed: true },
        { value: this.newValue, added: true }
      ];
    }

    let bestPath = [{ newPos: -1, components: [] }];

    // Seed editLength = 0, i.e. the content starts with the same values
    let oldPos = this.extractCommon(bestPath[0], this.newValue, this.oldValue, 0);
    if (bestPath[0].newPos + 1 >= this.newLen && oldPos + 1 >= this.oldLen) {
      // Identity per the equality and tokenizer
      return this.done([{ value: this.join(this.newValue, this.valueType), count: this.oldValue.length }]);
    }

    // Performs the length of edit iteration. Is a bit fugly as this has to support the
    // sync and async mode which is never fun. Loops over execCompareLength until a value
    // is produced, or until the edit length exceeds options.maxEditLength (if given),
    // in which case it will return undefined.
    if (this.callback) {
      this.exec(bestPath);
    } else {
      while (this.comparePath <= this.maxEditLength) {
        let ret = this.execCompareLength(bestPath);
        if (ret) {
          return ret;
        }
      }
    }
  }

}

diff-string.js

// LCS 算法,用于找出两端字符串中最长子串,基本思路为 动态规划算法 DP,算法复杂度 O(n^2)
function LcsFn(str1, str2) {
  const n1 = str1.length;
  const n2 = str2.length;
  // 建立空的二维数组,填充0,行列分别是两个字符串的长度
  const lcsArr = new Array(n1 + 1).fill(null).map((_, index) => new Array(n2 + 1).fill(0));
  // 使用动态规划存储子串长度
  // 遍历矩阵
  for (let i = 1; i < n1 + 1; i++) {
    for (let j = 1; j < n2 + 1; j++) {
      // 如果字符串的两个值相等,那么当前的值等于左上角的值 + 1
      if (str1[i - 1] === str2[j - 1]) {
        lcsArr[i][j] = lcsArr[i - 1][j - 1] + 1;
      }
      // 如果字符串的两个值不等,那么当前值等于上面的值和左边的值的最大值。
      else {
        lcsArr[i][j] = Math.max(lcsArr[i][j - 1], lcsArr[i - 1][j]);
      }
    }
  }
  const sameStr = getStr(str1, str2, lcsArr);
  return { str1, str2, lcsArr, sameStr }
}

// 根据二维数组,输出子串具体字符
function getStr(str1, str2, lcsArr) {
  const result = []
  let i = str1.length;
  let j = str2.length;
  // 使用双指针获取矩阵的索引
  while (i > 0 && j > 0) {
    // 如果索引对应的字符串一样,那么把相同的字符,放到结束数组中(就是公共字符)
    // 如果索引对应的字符串不一样,比较矩阵的值,那么一个指针减少1
    if (str1[i - 1] === str2[j - 1]) {
      result.unshift(str1[i - 1])
      i--
      j--
    } else if (lcsArr[i][j - 1] > lcsArr[i - 1][j]) {
      j--
    } else {
      i--
    }
  }
  return result;
}

let str1 = 'hello-Mike-Hi';      //  最长相同子串为:'e,l,o,b,g'   -- 长度9
let str2 = 'hello-Tom-Hi'; // -- 长度14

console.log(LcsFn(str1, str2));

diff-tree.js

// 获取当前内容和旧内容之间的差异
const getElementDiffValue = (currentContent, oldContent) => {
  // 初始化一个空 diff 对象,用于存储差异
  let diff = { value: [], changes: [] };

  // 生成id映射和id数组
  const { map: currentContentMap, ids: currentIds } = generateIdMapAndIds(currentContent);
  const { map: oldContentMap, ids: oldIds } = generateIdMapAndIds(oldContent);

  // 获取当前ids和旧ids之间的id差异
  const diffs = getIdDiffs(oldIds, currentIds);

  // 遍历全部不同的元素
  diffs.forEach(diffItem => {

    // 1、被删除的元素
    if (diffItem.removed) {
      diffItem.value.forEach(elementId => {
        diff.changes.push(elementId);
        // 获取被删除的元素
        const element = oldContentMap[elementId];
        // 生成被删除的元素的差异元素
        const diffElement = generatorDiffElement(element, TEXT_STYLE_MAP.DELETE, DELETED_STYLE);
        // 将被删除的元素的差异元素添加到value数组中
        diff.value.push(diffElement);
      });
    // 2、被添加的元素
    } else if (diffItem.added) {
      diffItem.value.forEach(elementId => {
        diff.changes.push(elementId);
        // 获取被添加的元素
        const element = currentContentMap[elementId];
        // 生成被添加的元素的差异元素
        const diffElement = generatorDiffElement(element, TEXT_STYLE_MAP.ADD, ADDED_STYLE);
        // 将被添加的元素的差异元素添加到value数组中
        diff.value.push(diffElement);
      });
    // 3、被修改的元素
    } else {
      diffItem.value.forEach(elementId => {
        const element = currentContentMap[elementId];
        // 更新差异
        updateDiffValue(diff, element, oldContentMap[element.id]);
      });
    }
  });

  // 返回差异
  return diff;
};

// 获取新旧id之间的差异:创建一个DiffText对象,用于比较新旧id
export const getIdDiffs = (oldIds, newIds) => {
  const diff = new DiffText(oldIds, newIds);
  return diff.getDiffs();
};


// 获取两个值之间的差异 return { value: [], change: [] }
export const getDiff = (currentValue = { children: [] }, oldValue = { children: [] }) => {
  // 如果两个值都为空,则返回空对象
  if (!currentValue && !oldValue) {
    return { value: [], changes: [] };
  }
  // 如果当前值为空,则返回旧值
  if (!currentValue && oldValue) {
    return { value: normalizeChildren(oldValue.children), changes: [] };
  }
  // 如果旧值为空,则返回当前值
  if (currentValue && !oldValue) {
    return { value: normalizeChildren(currentValue.children), changes: [] };
  }
  // 将当前值和旧值都转换为对象,并调用 normalizeChildren 函数,对 children 属性进行规范化
  const { version: currentVersion, children: currentContent } = { ...currentValue, children: normalizeChildren(currentValue.children) };
  const { version: oldVersion, children: oldContent } = { ...oldValue, children: normalizeChildren(oldValue.children) };
  // 如果两个值的版本号相同,则返回当前值
  if (currentVersion === oldVersion) {
    return { value: currentContent, changes: [] };
  } else {
    // 如果两个值的版本号不同,调用getElementDiffValue函数获取两个值之间的差异
  return getElementDiffValue(currentContent, oldContent);
  }
};

参考链接

jsdiff: https://github.com/kpdecker/jsdiff

An O(ND) Difference Algorithm and Its Variations: http://www.xmailserver.org/diff2.pdf

https://xueshu.baidu.com/usercenter/paper/show?paperid=1b0039833a74422f1de7e4286d40f30f

https://download.csdn.net/blog/column/8019278/88798916

https://www.cnblogs.com/wp-leonard/p/17888780.html

https://blog.csdn.net/m0_74931837/article/details/142443789

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/941384.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nacos源码搭建

拉取并配置代码 仓库地址 https://github.com/alibaba/nacos找到config 模块中找到 \resources\META-INF\mysql-schema.sql&#xff0c;在本地mysql中创建数据库nacos-config&#xff0c;将该脚本导入执行创建表。 找到console模块下的配置文件application.properties&#x…

C# Winfrom chart图 实例练习

代码太多了我就不展示了&#xff0c;贴一些比较有代表性的 成品效果展示&#xff1a; Excel转Chart示例 简单说一下我的思路 \ 先把Excel数据展示在dataGridView控件上 XLIST 为 X轴的数据 XLIST 为 Y轴的数据 ZLIST 为 展示的数据进行数据处理点击展示即可 // 将Excel数…

# 起步专用 - 哔哩哔哩全模块超还原设计!(内含接口文档、数据库设计)

↑ 上方下载文档 (大小374KB) 接口文档预览 (超过50个接口) 一、数据库25张表er-关系清晰构图&#xff01;(tip: 鼠标右键图片 > 放大图像) 二、难点/经验 详细说明 热门评论排序评论点赞列表|DTO封装经验分享|精华接口文档说明 组员都说喜欢分档对应枚举码 如果这篇文章…

【Go学习】从一个出core实战问题看Go interface赋值过程

0x01 背景 版本中一个同学找我讨论一个服务出core的问题&#xff0c;最终他靠自己的探索解决了问题&#xff0c;给出了初步的直接原因结论&#xff0c;"Go 中 struct 赋值不是原子的”。间接原因的分析是准确的&#xff0c;直接原因&#xff0c;我有点怀疑。当时写了一些…

leetcode之hot100---54螺旋矩阵(C++)

思路一&#xff1a;模拟 模拟螺旋矩阵的路径&#xff0c;路径超出界限&#xff0c;顺时针旋转&#xff0c;使用一个数组记录当前数字是否被访问到&#xff0c;直到所有的数字全部被访问 class Solution {//一个静态的常量数组&#xff0c;用于标记螺旋矩阵的移动方向(行列变化…

新能源汽车锂离子电池各参数的时间序列关系

Hi&#xff0c;大家好&#xff0c;我是半亩花海。为了进一步开展新能源汽车锂离子电池的相关研究&#xff0c;本文主要汇总并介绍了电动汽车的锂离子电池的各项参数&#xff0c;通过 MATLAB 软件对 Oxford Dataset 的相关数据集进行数据处理与分析&#xff0c;进一步研究各项参…

FastStone 10.x 注册码

简介 FastStone Capture是一款经典好用的屏幕截图软件&#xff0c;在屏幕截图领域具有广泛的应用和众多优势。 软件基本信息 FastStone Capture体积小巧&#xff0c;占用内存少&#xff0c;这使得它在运行时不会给计算机系统带来过多的负担&#xff0c;即使在配置较低的电脑…

AI合成图片是什么意思?有什么用?

随着人工智能的发展&#xff0c;现在市面上出现了很多对企业帮助很大的AI工具&#xff0c;比如说AI合成图片、AI换模特、AI穿衣、AI图片设计等等&#xff0c;下面小编就以AI合成图片为例&#xff0c;为大家详细介绍下。 一、AI合成图片是什么意思? AI合成图片主要就是指利用人…

【示例】Vue AntV G6 base64自定义img 动画效果,自适应宽高屏

需求&#xff1a;拓扑图中需要用动画的线条连接node&#xff0c;在此之前将HTML页面改成了vue页面。需要使用到G6的registerEdge 自定义边&#xff0c;小车的图片需要转成base64格式&#xff08;并翻转&#xff09;&#xff0c;可以通过base64转image查看原来的样子。 另外&am…

MySQL的分析查询语句

【图书推荐】《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;》-CSDN博客 《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;&#xff08;数据库技术丛书&#xff09;》(王英英)【摘要 书评 试读】- 京东图书 (jd.com) MySQL9数据库技术_夏天又到了…

【递归,搜索与回溯算法 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)

优美的排列 题目解析 算法原理 解法 &#xff1a;暴搜 决策树 红色剪枝&#xff1a;用于剪去该节点的值在对应分支中&#xff0c;已经被使用的情况&#xff0c;可以定义一个 check[ ] 紫色剪枝&#xff1a;perm[i] 不能够被 i 整除&#xff0c;i 不能够被 per…

观察者模式(sigslot in C++)

大家&#xff0c;我是东风&#xff0c;今天抽点时间整理一下我很久前关注的一个不错的库&#xff0c;可以支持我们在使用标准C的时候使用信号槽机制进行观察者模式设计&#xff0c;sigslot 官网&#xff1a; http://sigslot.sourceforge.net/ 本文较为详尽探讨了一种观察者模…

GitCode 光引计划投稿|智能制造一体化低代码平台 Skyeye云

随着智能制造行业的快速发展&#xff0c;企业对全面、高效的管理解决方案的需求日益迫切。然而&#xff0c;传统的开发模式往往依赖于特定的硬件平台&#xff0c;且开发过程繁琐、成本高。为了打破这一瓶颈&#xff0c;Skyeye云应运而生&#xff0c;它采用先进的低代码开发模式…

高校就业管理:系统设计与实现的全流程分析

3.1可行性分析 在项目进行开发之前&#xff0c;必须要有可行性分析报告&#xff0c;分别从技术角度&#xff0c;经济角度&#xff0c;操作角度上面进行分析&#xff0c;经过可行性分析是实现科学开发的必要步骤。 3.1.1技术可行性 从技术的角度出发&#xff0c;目前采用开发的技…

超级AI图像放大工具Upscayl:让你的照片细节更清晰,色彩更鲜艳!

前言 Hello大家好&#xff0c;我又来推荐非常好用的AI图片无损放大器,模糊图片秒变高清&#xff0c;Upscayl是一个免费开源的AI图像超分辨率工具。它使用AI模型来通过猜测细节的方式增强图像并提高其分辨率。该工具适用于Linux、macOS和Windows操作系统 安装环境 [名称]&…

1.gitlab 服务器搭建流程

前提条件&#xff1a; 一、服务器硬件水平 搭建gitlab服务器最低配置要求2核4G,低于这个配置的服务器运行效果很差。 gitlab官网&#xff1a;https://about.gitlab.com/ 下载地址&#xff1a;gitlab/gitlab-ce - Packages packages.gitlab.com 本机ubuntu 二、安装依赖 su…

Ai编程从零开始全栈开发一个后台管理系统之用户登录、权限控制、用户管理-前端部分(十二)

云风网 云风笔记 云风知识库 一、创建前端部分 1、vite初始化项目 npm create vitelatest admin-frontend – --template vue-ts 2、安装必要的依赖 npm install vue-router pinia axios element-plus element-plus/icons-vue安装完成后package.json如下&#xff1a; {&qu…

CVE-2024-34351 漏洞复现

CVE-2024-34351&#xff0c;由Next.js异步函数createRedirectRenderResult导致的SSRF。 影响版本&#xff1a;13.4.0< Next.js < 14.1.1 参考文章&#xff1a; Next.js Server-Side Request Forgery in Server Actions CVE-2024-34351 GitHub Advisory Database Gi…

怎么理解GKE Role-Based Access Control (RBAC) 和 Pod Security Policies (PSP)

怎么理解GKE Role-Based Access Control (RBAC) 和 Pod Security Policies (PSP) 理解 Google Kubernetes Engine (GKE) 中的角色基于访问控制&#xff08;RBAC&#xff09;和 Pod 安全策略&#xff08;PSP&#xff09;对于确保集群安全性至关重要。以下是对这两个概念的详细解…

什么是 DevOps 自动化?

DevOps 自动化是一种现代软件开发方法&#xff0c;它使用工具和流程来自动化任务并简化工作流程。它将开发人员、IT 运营和安全团队聚集在一起&#xff0c;帮助他们有效协作并交付可靠的软件。借助 DevOps 自动化&#xff0c;组织能够处理重复性任务、优化流程并更快地将应用程…