如何在 JavaScript 中对称地分割字符串?

4
我正在尝试在JavaScript中对字符串进行对称分割。这意味着,如果它有8个字符,则将其分成2个字符一组的块。如果它有9个字符,则分成3个字符一组的块。如果它有n个字符,则将其分为2个和3个字符一组的块,以形成镜像图像在中心。
const segmentNumber = (value) => {
  let array = value.split('')
  if (array.length % 3 === 0) {
    return chunkArray(array, 3).join('·')
  } else if (array.length % 2 === 0) {
    return chunkArray(array, 2).join('·')
  } else {
    let reversed = array.slice().reverse()
    let a = 0
    let ar = []
    let zr = []
    while (true) {
      ar.push(array.slice(a, a + 2).join(''))
      zr.push(reversed.slice(a, a + 2).join(''))
      array = array.slice(a + 2)
      reversed = reversed.slice(a + 2)
      a += 2
      let modulus
      if (array.length % 3 === 0) {
        modulus = 3
      } else if (array.length % 2 === 0) {
        modulus = 2
      }
      if (modulus) {
        return ar.concat(chunkArray(array, modulus)).concat(zr.reverse())
      }
    }
  }

  return
}

function chunkArray(arr, len) {

  var chunks = [],
      i = 0,
      n = arr.length;

  while (i < n) {
    chunks.push(arr.slice(i, i += len));
  }

  return chunks;
}

我可以将字符串划分成块,但不确定如何使其对称划分。例如:

  • 10: 2-3-3-2
  • 11: 2-2-3-2-2
  • 12: 3-3-3-3
  • 13: 2-3-3-3-2

如何生成此类模式,其中较小的数字在外部,而较大的数字3朝向中心,从而形成镜像?

如何改进我正在实现的方法?


将数据分成两个一组,然后重新匹配尽可能多的组合,以获得所需的输出。 - zenly
1
它只会是3和2吗? - Bravo
这是一个非常模糊的问题。不清楚输入可以是什么。 - Ram
是否没有限制,例如必须选择最多数量的3?因为某些长度可能具有对称块的不同组合。例如18可以是333333和22233222。根据您上面的示例,它可能只能产生3。 - NightEye
1
@Bravo,我相信这可能是正确的,如果可能的话,优化内部的3s,然后在外部填充2。 - NightEye
显示剩余5条评论
5个回答

9

一个通过调用自身解决问题的求解器

递归和生成器是解决这个问题的自然方法。我们可以使用归纳推理来为任何数字t编写segment(t) -

  1. 如果输入的t小于零,则没有有效的段。停止迭代。
  2. (归纳) t为非负数。如果t为零,则产生空段,[]
  3. (归纳) t为正数。产生单例段[t],对于范围1..t中的每个 i ,对于子问题t-i*2的每个段s,对称地在段前面和后面添加 i 并产生。

function* segment(t) {
  if (t < 0) return               // 1
  if (t == 0) return yield []     // 2
  yield [t]                       // 3
  for (let i = 1; i < t; i++)
    for (const s of segment(t - i * 2))
      yield [i, ...s, i]
}

console.log("== 7 ==")
for (const s of segment(7))
  console.log(s.join(" · "))
  
console.log("== 8 ==")
for (const s of segment(8))
  console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }

== 7 ==
7
1 · 5 · 1
1 · 1 · 3 · 1 · 1
1 · 1 · 1 · 1 · 1 · 1 · 1
1 · 2 · 1 · 2 · 1
2 · 3 · 2
2 · 1 · 1 · 1 · 2
3 · 1 · 3

== 8 ==
8
1 · 6 · 1
1 · 1 · 4 · 1 · 1
1 · 1 · 1 · 2 · 1 · 1 · 1
1 · 1 · 1 · 1 · 1 · 1 · 1 · 1
1 · 1 · 2 · 2 · 1 · 1
1 · 2 · 2 · 2 · 1
1 · 2 · 1 · 1 · 2 · 1
1 · 3 · 3 · 1
2 · 4 · 2
2 · 1 · 2 · 1 · 2
2 · 1 · 1 · 1 · 1 · 2
2 · 2 · 2 · 2
3 · 2 · 3
3 · 1 · 1 · 3
4 · 4

为什么是t - i * 2

子问题是t - i * 2,或者说是t减去两个i。后续的yield表达式会将两个i加到结果中,因此它们必须从子问题中减去以平衡方程。

for (const s of segment(t - i * 2)) // subtract two i from t
  yield [i, ...s, i]                // add two i to the segment

为什么要使用递归技术?

这种方法的优点很多 -

静态,任意分割大小
仅限2和3
模数算术
条件变量赋值
数组 .slice.reverse
硬编码的 .join
字符串转数字
parseInt
除法和舍入
找到每个组合
两个本地变量
两个简单的 if 条件
暂停、恢复或取消

可视化

给定segment(4) -

[4]                                  // t = 4

[1,            , 1]
    \        /
      ...[2]                         // t = 2

[1,                           , 1]
    \                       /
      ...[1,           , 1]
             \       /
               ...[]                 // t = 0

[2,           , 2]
    \       /
      ...[]                          // t = 0

[3,           , 3]
    \       /
      ...❌                         // t = -2

[4]
[1,2,1]
[1,1,1,1]
[2,2]

改变输出顺序

这不需要对算法进行手术式重构或调整思考问题的方式。通过改变yield表达式的顺序,可以改变输出的顺序 -

function* segment(t) {
  if (t < 0) return
  if (t == 0) return yield []
  for (let i = 1; i < t; i++)
    for (const s of segment(t - i * 2))
      yield [i, ...s, i]
  yield [t]   // ✅ yield singleton segment last
}

console.log("== 7 ==")
for (const s of segment(7))
  console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }

== 7 ==
1 · 1 · 1 · 1 · 1 · 1 · 1
1 · 1 · 3 · 1 · 1
1 · 2 · 1 · 2 · 1
1 · 5 · 1
2 · 1 · 1 · 1 · 2
2 · 3 · 2
3 · 1 · 3
7

最小分段长度

也许你希望最小的分段长度是 2 或者 3。通过添加 min 参数,调用者可以决定最小分段长度而不是将其硬编码到 segment 函数中 -

function* segment(t, min = 0) { // ✅ add min parameter
  if (t < min) return              // ✅ t < min
  if (t == 0) return yield []
  for (let i = Math.max(1, min); i < t; i++) // ✅ max(1, min)
    for (const s of segment(t - i * 2, min)) // ✅ pass min
      yield [i, ...s, i]
  yield [t]
}

console.log("== 18 ==")
for (const s of segment(18, 3)) // ✅ segments of 3 or greater
  console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }

最大段长度

类似地,可以添加max参数来控制最大段长度。避免在函数中硬编码此值,以保留对调用者的更高灵活性 -

function* segment(t, min = 0, max = t) { // ✅ add max parameter
  if (t < min) return
  if (t == 0) return yield []
  for (let i = Math.max(1, min); i < t; i++)
    for (const s of segment(t - i * 2, min, max)) // ✅ pass max
      yield [i, ...s, i]
  if (t <= max) yield [t] // ✅ if (t <= max)
}

console.log("== 18 ==")
for (const s of segment(18, 3, 8)) // ✅ segments between 3 and 8
  console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }

== 18 ==
3 · 3 · 6 · 3 · 3
3 · 4 · 4 · 4 · 3
4 · 3 · 4 · 3 · 4
5 · 8 · 5
6 · 6 · 6
7 · 4 · 7

如果您希望数字朝中心增加,那么需要添加另一个可配置参数“init”。正如您所看到的,每个微妙的标准都可以通过对原始算法进行小幅调整来仔细添加。

function* segment(t, min = 0, max = t, init = 1) { // ✅ init = 1
  if (t < min || t < init) return // ✅ || t < init
  if (t == 0) return yield []
  for (let i = Math.max(init, min); i < t; i++) // ✅ init
    for (const s of segment(t - i * 2, min, max, i + 1)) // ✅ i + 1
      yield [i, ...s, i]
  if (t <= max) yield [t]
}

console.log("== 36 ==")
for (const s of segment(36, 2, 9))
  console.log(s.join(" · "))
.as-console-wrapper { min-height: 100%; top: 0; }

== 36 ==
2 · 3 · 4 · 5 · 8 · 5 · 4 · 3 · 2
2 · 5 · 7 · 8 · 7 · 5 · 2
3 · 4 · 7 · 8 · 7 · 4 · 3
3 · 5 · 6 · 8 · 6 · 5 · 3

分割字符串

要将字符串分割,我们可以编写split函数,该函数接受一个字符串和由segment返回的值 -

const split = (t, [s, ...segments]) =>
  s == null
    ? []
    : [t.substring(0, s), ...split(t.substring(s), segments)]

function* segment(t) {
  if (t < 0) return
  if (t == 0) return yield []
  for (let i = 1; i < t; i++)
    for (const s of segment(t - i * 2))
      yield [i, ...s, i]
  yield [t]
}
    
const word = "function"
for (const s of segment(word.length)) // ✅ segment word.length
  console.log(split(word, s).join(" · ")) // ✅ split word using s
.as-console-wrapper { min-height: 100%; top: 0; }

f · u · n · c · t · i · o · n
f · u · n · ct · i · o · n
f · u · nc · ti · o · n
f · u · ncti · o · n
f · un · c · t · io · n
f · un · ct · io · n
f · unc · tio · n
f · unctio · n
fu · n · c · t · i · on
fu · n · ct · i · on
fu · nc · ti · on
fu · ncti · on
fun · c · t · ion
fun · ct · ion
func · tion
function

优化

通过消除一些组合的检查,您可以加快算法速度。外部for循环从1..t,但可以缩短为1..Math.floor(t/2)。这提高了segment的性能,但增加了一些复杂性。出于清晰起见,此内容被省略,仅供读者更新。

不使用生成器

虽然生成器非常适合组合问题,但并非必需。我们可以将整个程序提升到一个数组上下文中,并使segment返回结果数组而不是迭代器。请注意,人机交互性不如以前那么好,数据嵌套级别被强制增加了一个。但它确实遵循与原始算法相同的归纳推理 -

function segment(t) {
  if (t < 0) return []            // if (t < 0) return 
  if (t == 0) return [[]]         // if (t == 0) return yield []
  return [                        //   
    [t],                          // yield [t]
    ...range(1, t).flatMap(i =>   // for (let i = 0; i<t; i++)
      segment(t - i * 2).map(s => //   for (const s of segment(t - i * 2))
        [[i, ...s, i]]            //     yield [i, ...s, i]
      )
    )
  ]
}

function range(a, b) {
  return Array.from(Array(b - a), (_, n) => n + a)
}

console.log("== 7 ==")
for (const s of segment(7))
  console.log(s.join(" · "))

== 7 ==
7
1,5,1
1,1,3,1,1
1,1,1,1,1,1,1
1,2,1,2,1
2,3,2

生成器的妙用,我收藏了这个问答。 - zer00ne
太棒了!现在我想知道,如何在命令式编程中实现完整的“字符串分割”而不使用递归或函数式编程? - Lance

1
function segmentNumber(value)
{
  for (let i = 0; i <= value; i++)
  {
    d = (value - 3 * i)/4
    if (d >= 0 && d == parseInt(d))
    {
      return Array(d).fill(2).concat(Array(i).fill(3), Array(d).fill(2))
    }
  }
}

1

这个输出符合您的要求吗?我不确定您的输入是什么,所以我只在您提供的示例值上进行了测试。

function segmentNumber(length) {
  let outside = 0;
  if (length < 6)
    return 'Only 6 and greater';

  if(length % 3 == 0) 
    return new Array(length / 3).fill(3).join('-');
  else {
    while(length % 3 != 0 && length > 6) {
      outside++;
      length = length - 4;
    }
    if(length == 4) 
      return new Array(Math.floor(++outside * 2)).fill(2).join('-');
    else
      return [...new Array(outside).fill(2), 
              ...new Array(Math.floor(length / 3)).fill(3), 
              ...new Array(outside).fill(2)].join('-');
  }
}

console.log(segmentNumber(10))
console.log(segmentNumber(11))
console.log(segmentNumber(12))
console.log(segmentNumber(13))


1

这将生成大于6的数字模式,并始终在外部至少有一个“2”(除了9)

const splitter = n => {
  if (n < 7) {
    throw 'bad'
  }
  let twos = 0;
  let threes = Math.ceil(n / 3);
  // 9 is an edge case
  if (n !== 9) {
    let remain;
    do {
      --threes;
      remain = n - threes * 3;
    } while (remain % 4);
    if (threes < 0) {
      threes = n / 3;
      remain = 0;
    }
    twos = remain / 4;
  }
  return `${'2'.repeat(twos)}${'3'.repeat(threes)}${'2'.repeat(twos)}`.split('');
}
for (let i = 7; i < 50; ++i) console.log(i, splitter(i).join('-'))

简单更改...

const splitter = n => {
  if (n < 7) {
    throw 'bad'
  }
  let twos = 0;
  let threes = Math.floor(n / 3) + 1;
  // 9 is an edge case
  let remain;
  do {
    --threes;
    remain = n - threes * 3;
  } while (remain % 4);
  if (threes < 0) {
    threes = n / 3;
    remain = 0;
  }
  twos = remain / 4;
  return `${'2'.repeat(twos)}${'3'.repeat(threes)}${'2'.repeat(twos)}`.split('');
}
for (let i = 7; i < 50; ++i) console.log(i, splitter(i).join('-'))


0

标准

从我从原帖和评论中了解到的,目标是从给定的整数返回对称的2和3系列。因此,这里是我尝试遵循的一些标准:

  • 从结果的开头和结尾开始,2s然后3s被定位到中心:

    10 => 2 3 3 2
    
  • 不应接受低于4和5的数字。因此,对于任何小于6的数字都将设置限制(4是对称的,但我不想编写额外的代码来包括它)

  • 如果给定的数字是奇数,则中心将有一个3。

    11 => 2 2 3 2 2
    
  • 对称模式只需要构建一半的模式,因此将数字除以2(如果是奇数,则在除以2之前减去3)。

    if (int < 6) return "必须是大于5的整数";
    let parity = int & 1 ? 'odd' : 'even';
    if (parity === 'odd') {
      int = int - 3;
    }
    side = int / 2;
    
  • 找出需要多少次迭代才能将数字除以3,然后将剩余部分(如果有)除以2。

  • 剩余数字只能是4、2或0,以保持对称性。如果剩余数字为1,则将除以3的总迭代次数减1,剩余数字1将变为4。

  • 运行除以3和除以2的两个单独的循环。

    let temp = Math.floor(side / 3);
    let rem = side % 3;
    if (rem === 1) {
      temp = temp - 1;
      rem = 4;
    }
    for (let i = 0; i < temp; i++) {
      result.push(3);
    }
    for (let i = 0; i < rem / 2; i++) {
      result.push(2);
    }
    
  • 接下来,使用.slice(0)复制result数组并.reverse()它。如果parity"odd",则在result的前面添加unshift(3)。最后,在result数组的前面.concat()反转的数组,然后.join(' ')成字符串。

    const left = result.slice(0).reverse();
    if (parity === 'odd') {
      result.unshift(3);
    }
    return left.concat(result).join(' ');
    
    

// Utility function
const log = (data, str = true) => {
  if (!str) return console.log(data);
  return console.log(JSON.stringify(data));
}

const x23 = int => {
  let result = [];
  let side, odd;
  if (int < 6) return "Must be an integer greater than 5";
  let parity = int & 1 ? 'odd' : 'even';
  if (parity === 'odd') {
    int = int - 3;
  }
  side = int / 2;

  let temp = Math.floor(side / 3);
  let rem = side % 3;
  if (rem === 1) {
    temp = temp - 1;
    rem = 4;
  }
  for (let i = 0; i < temp; i++) {
    result.push(3);
  }
  for (let i = 0; i < rem / 2; i++) {
    result.push(2);
  }

  const left = result.slice(0).reverse();
  if (parity === 'odd') {
    left.push(3);
  }
  return left.concat(result).join(' ');
}

log('5: '+x23(5));
log('6: '+x23(6));
log('7: '+x23(7));
log('8: '+x23(8));
log('9: '+x23(9));
log('10: '+x23(10));
log('11: '+x23(11));
log('12: '+x23(12));
log('13: '+x23(13));
log('14: '+x23(14));
log('15: '+x23(15));
log('16: '+x23(16));
log('17: '+x23(17));
log('18: '+x23(18));
log('19: '+x23(19));
log('20: '+x23(20));
log('21: '+x23(21));
log('22: '+x23(22));
log('23: '+x23(23));
log('24: '+x23(24));
log('53: '+x23(53));
log('99: '+x23(99));
.as-console-wrapper {
  min-height: 100% !important;
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接