如何使用JavaScript或TypeScript打乱项目数组

877 阅读7分钟

在本文中,我们将探索如何使用 TypeScript 或 JavaScript(如果您愿意的话)以多种不同方式对项目数组进行混洗。

先决条件:

  • 了解 TypeScript 或 JavaScript
  • 对 For 循环和数组的基本理解

以下示例是用 TypeScript 编写的,但它们的工作方式与使用纯 JavaScript 的方式完全相同。您只需要从:type所有函数参数中删除语法。

(更多优质教程:java567.com,搜索"javascript")

方法一:Fisher-Yates 排序算法

该算法的基本前提是迭代项目,将数组中的每个元素与数组中剩余的未打乱部分中随机选择的元素交换。

让我们在实践中看看这个,因为它会帮助您更好地形象化它:

 // declare the function 
 const shuffle = (array: string[]) => { 
   for (let i = array.length - 1; i > 0; i--) { 
     const j = Math.floor(Math.random() * (i + 1)); 
     [array[i], array[j]] = [array[j], array[i]]; 
   } 
   return array; 
 }; 
   
 // Usage 
 const myArray = ["apple", "banana", "cherry", "date", "elderberry"]; 
 const shuffledArray = shuffle(myArray); 
 ​
 console.log(shuffledArray); 

解释

首先,您创建一个 for 循环。这将允许您遍历数组中的每个项目,将其位置与数组中的另一个项目交换。

然后创建i变量并为其赋值length of the array - 1。

您这样做是因为我们从数组的最后一个元素开始,并且所有数组索引都从 0 开始——因此最后一个索引将为 4(因为数组中有 5 个项目)。

如果您尝试使用myArray[i]等于i(5长度)进行访问,它会抛出一个异常,说明索引 5 处没有项目。因此我们从长度中减去 1。

通过从最后一个元素开始并向后工作,您可以保证接近数组末尾的元素有相同的机会与任何其他元素交换。

如果你要从头到尾打乱数组,那么靠近数组开头的元素将有更高的机会被交换多次,从而导致有偏差或不均匀的打乱。‌‌‌‌ 然后你创建一个变量,它将j是用于大交换的索引指针。

然后将 index 处的数组分配i给 index 处的数组j,反之亦然。这将交换值并为数组中的每个项目将它们打乱。

数组解构赋值解释

该语法[array[i], array[j]] = [array[j], array[i]]称为数组解构赋值。它允许在不需要临时变量的情况下在两个变量或数组元素之间交换值。

以下是数组解构分配在使用 Fisher-Yates 混洗算法混洗数组的上下文中的工作方式:

  • array[i]并array[j]表示数组中需要交换的两个元素。
  • [array[j], array[i]]``array[j]创建一个临时数组,其中包含和的值array[i],但顺序相反。
  • 通过分配[array[j], array[i]]给,和[array[i], array[j]]的值就地交换。array[j]``array[i]

您可以在此处了解有关数组解构的更多信息。‌‌‌‌如果我们逐步打印出函数的步骤,我们将得到以下内容:

 // starting array 
 ["apple", "banana", "cherry", "date", "elderberry"]; 
 ​
 //1st Iteration - Swap elderberry with date 
 i = 4; // elderberry 
 j = 3; // date 
 [("apple", "banana", "cherry", "elderberry", "date")]; 
 ​
 // 2nd Iteration -  Swap elderberry with apple 
 i = 3; // 
 elderberry j = 0; // apple 
 [("elderberry", "banana", "cherry", "apple", "date")]; 
 ​
 // 3rd Iteration - Swap cherry with banana 
 i = 2; // cherry 
 j = 1; // banana 
 [("elderberry", "cherry", "banana", "apple", "date")]; 
 ​
 // 4th Iteration - Swap cherry with itself (stays where it is) 
 i = 1; // cherry 
 j = 1; // cherry 
 [("elderberry", "cherry", "banana", "apple", "date")]; 

好了——我们已经使用 Fisher-Yates 算法对数组进行了混洗。

方法 2:使用sort()具有随机比较功能的方法

如果你不熟悉 JS 排序功能,我已经写了一篇关于如何使用它的教程,你可以在这里找到。

 const shuffle = (array: string[]) => { 
     return array.sort(() => Math.random() - 0.5); 
 }; 
 ​
 // Usage 
 const myArray = ["apple", "banana", "cherry", "date", "elderberry"]; 
 const shuffledArray = shuffle(myArray); 
 console.log(shuffledArray);

这是一个简单的 sorting() 函数,它返回一个随机数,结果可以是负数、0 或正数。‌‌‌‌该sort()方法在内部比较数组中的元素对,并根据返回值确定它们的相对顺序比较功能。

  • 如果比较函数返回负值,则第一个元素被认为较小,应放在排序数组中的第二个元素之前。
  • 如果比较函数返回正值,则第一个元素被认为更大,应放在排序数组中的第二个元素之后
  • 如果比较函数返回 0,则元素的相对顺序保持不变。

返回什么Math.random()?

当你调用Math.random()它时会生成一个伪随机数,因为你可能知道,没有什么是真正随机的哈哈。‌‌‌‌ “伪随机”意味着生成的数字看起来是随机的但实际上是由确定性算法确定的(两者之间不同JS 引擎实现)。‌‌‌它返回的数字将始终是一个介于 0 和 1 之间的浮点数。浮点数(通常称为“浮点数”)是可以为正数或为负数并且可以包含小数部分的数字。浮点数的示例包括3.14、-0.5、1.0、2.71828等。

为什么要从 的结果中减去 0.5 Math.random()?

通过从 Math.random() 的结果中减去 0.5,您可以引入一个介于 -0.5 和 0.5 之间的随机值。该随机值将导致比较函数以随机方式为不同的元素对返回负值、正值或零值。因此, sort() 方法随机打乱数组。

方法三:使用JSArray.map()函数

该.map()函数允许您迭代数组的每个元素,并根据提供的映射函数将它们转换为新值。该map()函数返回一个包含转换后值的新数组,而原始数组保持不变。

 const shuffle = (array: string[]) => { 
     return array.map((a) => ({ sort: Math.random(), value: a }))
         .sort((a, b) => a.sort - b.sort)
         .map((a) => a.value); 
 }; 
 ​
 // Usage 
 const myArray = ["apple", "banana", "cherry", "date","elderberry"]; const shuffledArray = shuffle(myArray); 
 console.log(shuffledArray); 

在这里,您遍历数组并Math.random()在函数中使用与上面示例中相同的函数map(),返回具有排序编号和值的对象数组。‌‌‌‌然后您可以使用该函数sort()根据以下条件对数组进行排序这些值,然后再次调用该map()函数以创建一个值数组(即字符串名称)。

 const shuffle = (array: string[]) => { 
   const shuffled = array.slice(); 
   let currentIndex = shuffled.length; 
   let temporaryValue, randomIndex; 
   while (currentIndex !== 0) { 
     randomIndex = Math.floor(Math.random() * currentIndex); 
     currentIndex -= 1; 
     temporaryValue = shuffled[currentIndex]; 
     shuffled[currentIndex] = shuffled[randomIndex]; 
     shuffled[randomIndex] = temporaryValue; 
   }
   return shuffled; 
 }; 
 ​
 // Usage 
 const myArray = ["apple", "banana", "cherry", "date", "elderberry"]; const shuffledArray = shuffle(myArray); 
 console.log(shuffledArray);

结论

我推荐 Fisher-Yates 算法方法,因为它的时间复杂度很低,因为它完全取决于数组的大小。它的时间复杂度可以看作O(n),这意味着将输入大小加倍将大致使执行时间加倍。同样,如果输入大小减半,执行时间也将大约减半。

无需过多介绍:

  • O代表增长或复杂性等级的顺序。
  • n指的是输入的大小,通常由变量 表示n。

这意味着函数的复杂度随着 n(输入)大小的变化而变化——例如:复杂度 x 2(数组中的 2 个项目)与复杂度 x 10(数组中的 10 个项目)。因此,数组越大,洗牌的复杂性和时间就越大。

在对大型数组进行洗牌时,这一点值得注意。可能值得研究其他方法,或者在将数组重新组合在一起之前对数组进行分块并并行运行改组。

此方法还允许更轻松地对任何类型的数组进行洗牌,而不仅仅是洗牌string[]。它在使用 TypeScript 泛型时也能很好地工作。这允许将任何类型的数组传递给函数,并对其进行混洗。

 const shuffle = <T>(array: T[]) => { 
   for (let i = array.length - 1; i > 0; i--) { 
     const j = Math.floor(Math.random() * (i + 1)); 
     [array[i], array[j]] = [array[j], array[i]]; 
   }
   return array; 
 }; 
 ​
 const strings = ["apple", "banana", "cherry", "date", "elderberry"]; 
 const users = [ { name: "John", surname: "Doe" }, { name: "Jane", surname: "Doe" }];
 ​
 const shuffledArray = shuffle(strings); 
 const shuffledObjects = shuffle(users); 
 ​
 console.log(shuffledArray); 
 console.log(shuffledObjects); 

我希望你发现这篇文章有用,并且已经学会了如何轻松地随机排列一组项目。您可以将其用于多个用例,例如:

  • 随机播放歌曲列表
  • 洗牌团队成员名单以确定轮换系统
  • 创建随机问题/订单随机数据样本的测验,例如客户评论/反馈。

(更多优质教程:java567.com,搜索"javascript")