先上效果
代码思路
对两个字符串使用 LCS 算法,获取最长子串,再通过这个最长子串找出新旧字符串中的其他字符段,对应为新增字符段、删除字符段,再以新增、删除、原有顺序拼接diff字符串。
代码实现
LCS 算法
// lcs 算法,用于找出两端字符串中最长子串,基本思路为动态规划
export function LcsFn(str1, str2) {
const n1 = str1.length;
const n2 = str2.length;
// 建立二维数组,使用动态规划存储子串长度
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++) {
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,
j = str2.length
while (i > 0 && j > 0) {
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 = 'eleocfvfbg'; // 最长相同子串为:'e,l,o,b,g' -- 长度9
let str2 = 'ledfdkakglowbgked'; // -- 长度14
输出 LCS算法的二维数组 并 找出最长相同子串结果:
渲染字符串差异
根据最长相同子串与新旧字符串,先分别切割出新旧子串中的非相同部分,把他们标记为删除:‘ - ’、新增:‘ + ’、相同:‘ = ’。再按顺序以删除、新增、相同部分拼接,根据标记设定不同的类名。
/**
* @param {str1, str2, sameStr}
* str1: 旧字符串
* str2: 新字符串
* sameStr: 新旧字符串的最长子字符串
*
* desc:根据相同子串,获取新旧字符串的非最长相同子串的部分,
* 旧子串部分前面设置为‘ - ’,新字符部分设置为 ‘ + ’,相同部分设置为 ‘ = ’
* 根据‘- + =’设置不同className
*/
function getStrPlace(str1, str2, sameStr) {
let oldSameItem = 0; // 旧字符串的上一个相同子字符节点
let newSameItem = 0; // 新字符串的上一个相同子字符节点
let lastOldItem = 0; // 旧字符串的上一个切割节点
let lastNewItem = 0; // 新字符串的上一个切割节点
const deleteOldItems = []; // 存储对比后的删除字符段
const addNewItems = []; // 存储对比后的新增的字符段
// 获取旧字符串中删除的字符段
for (let i = 0; i < str1.length; i++) {
if(str1[i] === sameStr[oldSameItem]) {
deleteOldItems.push(i - lastOldItem > 0 ? str1.slice(lastOldItem, i) : '');
lastOldItem = i + 1;
oldSameItem++
if(oldSameItem == sameStr.length) {
deleteOldItems.push(i < str1.length -1 ? str1.slice(i + 1, str1.length) : '');
break;
}
}
}
// 获取新字符串中新增的字符段
for (let i = 0; i < str2.length; i++) {
if(str2[i] === sameStr[newSameItem]) {
addNewItems.push(i - lastNewItem > 0 ? str2.slice(lastNewItem, i) : '');
lastNewItem = i + 1;
newSameItem++
if(newSameItem == sameStr.length) {
addNewItems.push(i < str2.length -1 ? str2.slice(i + 1, str2.length) : '');
break;
}
}
}
// 依次将删除、新增、相同,标记后插入数组
let index = 0;
const resultArr = sameStr.reduce((lastValue, currentValue, currentIndex) =>{
const newArr = [];
if (deleteOldItems[index]) {
newArr.push('-');
newArr.push(deleteOldItems[index]);
}
if (addNewItems[index]) {
newArr.push('+');
newArr.push(addNewItems[index]);
}
newArr.push('=');
newArr.push(currentValue);
index++;
const returnArr = lastValue.concat(newArr);
// 判断最末尾有无字符串增删,有则插入
if(currentIndex === sameStr.length - 1) {
const lastArr = [];
if (deleteOldItems[index]) {
lastArr.push('-');
lastArr.push(deleteOldItems[index]);
}
if (addNewItems[index]) {
lastArr.push('+');
lastArr.push(addNewItems[index]);
}
return returnArr.concat(lastArr);
}
return returnArr
},[]);
// 渲染结果数组
renderDiffStr(resultArr);
}
// 输出resultArr:
// ['+', 'l', '=', 'e', '+', 'dfdkakg', '=', 'l', '-', 'e', '=', 'o', '-', 'cfvf', '+', 'w', '=', 'b', '=', 'g', '+', 'ked']
接下来渲染 resultArr
function renderDiffStr(resultArr) {
const diffStr = document.getElementById("diffStr");
// 清除子节点,防止多次diff插入的子节点重复
while (diffStr.firstChild) {
diffStr.firstChild.remove();
}
// 寻找
for (let i = 0; i < resultArr.length; i++) {
if(i % 2 == 1) {
const spanElement = document.createElement('span');
spanElement.innerHTML = resultArr[i];
spanElement.className = getElementClass(resultArr[i - 1])
diffStr.appendChild(spanElement);
}
}
}
function getElementClass(type) {
console.log(type);
switch (type) {
case "-" : return DELETE_ELEMENT;
case "+" : return ADD_ELEMENT;
default : return OLD_ELEMENT;
}
}
渲染效果:
最终文件(相同文件夹)
Lcs.mjs
// lcs 算法,用于找出两端字符串中最长子串,基本思路为动态规划
export function LcsFn(str1, str2) {
const n1 = str1.length;
const n2 = str2.length;
// 建立二维数组,使用动态规划存储子串长度
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++) {
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,
j = str2.length
while (i > 0 && j > 0) {
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;
}
Lcs.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="input-container">
<div class="input-old-string">
<span>请输入旧字符串:</span>
<input class="inputString" type="text" value="eleocfvfbg">
</div>
<div class="input-new-string">
<span>请输入新字符串:</span>
<input class="inputString" type="text" value="ledfdkakglowbgked">
</div>
<button id="strDiff">字符串diff</button>
</div>
<div>
<h1 class="str1"></h1>
<h1 class="str2"></h1>
</div>
<div class="container">
</div>
<div>
<span>最长相同子串:</span>
<h1 class="sameStr">
</h1>
</div>
<div>
<span>diff结果:</span>
<h1 id="diffStr"></h1>
</div>
</body>
</html>
<script type="module">
import { LcsFn } from './Lcs.mjs'
const DELETE_ELEMENT = 'delete-element';
const ADD_ELEMENT = 'add-element';
const OLD_ELEMENT = 'old-element';
const strDiff = document.getElementById('strDiff');
const inputString = document.getElementsByClassName('inputString');
window.onload = () => {
inputString[0].focus();
}
strDiff.onclick = () => update(inputString[0].value, inputString[1].value);
function update(strOld, strNew) {
const LcsData = LcsFn(strOld, strNew);
const {str1, str2, lcsArr, sameStr} = LcsData
// lcs二维数组展示赋值
let divEle = document.getElementsByClassName('container');
while (divEle[0].firstChild) {
divEle[0].firstChild.remove();
}
for (let i = 0; i < lcsArr.length; i++) {
const divElement = document.createElement('div');
for (let j = 0; j < lcsArr[0].length; j++) {
const spanElement = document.createElement('span');
spanElement.innerHTML = i == 0 ? j : ( j == 0 ? i : lcsArr[i][j]);
spanElement.className = i == 0 ? 'head' : ( j == 0 ? 'head' : '')
divElement.appendChild(spanElement);
}
divEle[0].appendChild(divElement);
}
// lcs前后两字符串展示以及最长相同子串
const str1Ele = document.getElementsByClassName('str1')[0];
const str2Ele = document.getElementsByClassName('str2')[0];
const sameStrEle = document.getElementsByClassName('sameStr')[0];
str1Ele.innerHTML = str1
str2Ele.innerHTML = str2
sameStrEle.innerHTML = sameStr.toString();
getStrPlace(str1, str2, sameStr);
}
/**
* @param {str1, str2, sameStr}
* str1: 旧字符串
* str2: 新字符串
* sameStr: 新旧字符串的最长子字符串
*
* desc:根据相同子串,获取新旧字符串的非最长相同子串的部分,
* 旧子串部分前面设置为‘ - ’,新字符部分设置为 ‘ + ’,相同部分设置为 ‘ = ’
* 根据‘- + =’设置不同className
*/
function getStrPlace(str1, str2, sameStr) {
let oldSameItem = 0; // 旧字符串的上一个相同子字符节点
let newSameItem = 0; // 新字符串的上一个相同子字符节点
let lastOldItem = 0; // 旧字符串的上一个切割节点
let lastNewItem = 0; // 新字符串的上一个切割节点
const deleteOldItems = []; // 存储对比后的删除字符段
const addNewItems = []; // 存储对比后的新增的字符段
// 获取旧字符串中删除的字符段
for (let i = 0; i < str1.length; i++) {
if(str1[i] === sameStr[oldSameItem]) {
deleteOldItems.push(i - lastOldItem > 0 ? str1.slice(lastOldItem, i) : '');
lastOldItem = i + 1;
oldSameItem++
if(oldSameItem == sameStr.length) {
deleteOldItems.push(i < str1.length -1 ? str1.slice(i + 1, str1.length) : '');
break;
}
}
}
// 获取新字符串中新增的字符段
for (let i = 0; i < str2.length; i++) {
if(str2[i] === sameStr[newSameItem]) {
addNewItems.push(i - lastNewItem > 0 ? str2.slice(lastNewItem, i) : '');
lastNewItem = i + 1;
newSameItem++
if(newSameItem == sameStr.length) {
addNewItems.push(i < str2.length -1 ? str2.slice(i + 1, str2.length) : '');
break;
}
}
}
// 依次将删除、新增、相同,标记后插入数组
let index = 0;
const resultArr = sameStr.reduce((lastValue, currentValue, currentIndex) =>{
const newArr = [];
if (deleteOldItems[index]) {
newArr.push('-');
newArr.push(deleteOldItems[index]);
}
if (addNewItems[index]) {
newArr.push('+');
newArr.push(addNewItems[index]);
}
newArr.push('=');
newArr.push(currentValue);
index++;
const returnArr = lastValue.concat(newArr);
// 判断最末尾有无字符串增删,有则插入
if(currentIndex === sameStr.length - 1) {
const lastArr = [];
if (deleteOldItems[index]) {
lastArr.push('-');
lastArr.push(deleteOldItems[index]);
}
if (addNewItems[index]) {
lastArr.push('+');
lastArr.push(addNewItems[index]);
}
return returnArr.concat(lastArr);
}
return returnArr
},[]);
console.log(resultArr);
// 渲染结果数组
renderDiffStr(resultArr);
}
function renderDiffStr(resultArr) {
const diffStr = document.getElementById("diffStr");
// 清除子节点,防止多次diff插入的子节点重复
while (diffStr.firstChild) {
diffStr.firstChild.remove();
}
// 寻找
for (let i = 0; i < resultArr.length; i++) {
if(i % 2 == 1) {
const spanElement = document.createElement('span');
spanElement.innerHTML = resultArr[i];
spanElement.className = getElementClass(resultArr[i - 1])
diffStr.appendChild(spanElement);
}
}
}
function getElementClass(type) {
console.log(type);
switch (type) {
case "-" : return DELETE_ELEMENT;
case "+" : return ADD_ELEMENT;
default : return OLD_ELEMENT;
}
}
</script>
<style>
.container span {
display: inline-block;
width: 30px;
height: 30px;
box-sizing: border-box;
border: 1px solid #33333315;
line-height: 30px;
text-align: center;
background-color: #a1f28672;
}
.head {
background-color: #f5c3c3;
color: #0000003d;
}
input {
border: none;
outline: none;
}
.delete-element {
color: red;
text-decoration-line: line-through;
}
.add-element {
color: rgb(9, 235, 9);
}
.old-element {
color: black;
}
</style>