数组去重、排序、扁平化

本文详细介绍了JavaScript中数组去重的多种方法,包括双层for循环、indexOf、includes、filter+indexOf、相邻元素去重、对象属性去重、Set与解构赋值、reduce等。同时,对数组排序如冒泡排序、选择排序、插入排序、归并排序和快速排序进行了讲解。此外,还探讨了数组扁平化的各种实现,包括ES6的flat()方法、递归+forEach、递归+reduce+concat、扩展运算符+some、toString+split以及lodash的_.flatMapDepth方法。

数组去重、排序、扁平化

1. 数组去重

1-1 双层 for 循环

  • 定义一个新数组,并存放原数组的第一个元素
  • 遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对
  • 如果不重复则添加到新数组中,重复则不添加,最后返回新数组
// 双重for循环去重
// 通过原始数组与新数组中元素对比来去重
function unique(arr) {
    // 判断是否为数组
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
	// 定义新数组 新数组第一个元素为元素数组第一个元素
	let newArr = [arr[0]]
	for (let i = 0; i < arr.length; i++) {
		console.log(arr[i])
		let repeat = false;
		for (let j = 0; j < newArr.length; j++) {
			console.log(newArr[j])
            // 判断如果元素相同则返回true然后继续迭代
			if (arr[i] === newArr[j]) {
				repeat = true;
				break;
			}
		}
        // repeat为false时 arr[i]与newArr[j]不相等时添加新元素
		if (!repeat) {
			console.log(newArr)
			newArr.push(arr[i])
			console.log(newArr)
		}
	}
    // 执行完返回新数组
	return newArr;
}

let arr = [1, 1, 2, 7, 7]
console.log(unique(arr))

1-2 indexOf 去重

数组的 indexOf() 方法可返回某个指定的元素在数组中首次出现的位置

  • 首先定义一个空数组 newArr,然后调用 indexOf 方法对原来的数组进行遍历判断
  • 如果元素不在新数组中,则将其 push 进新数组中,最后将新数组返回即可获得去重的数组
// 通过indexof获取元素首次出现的位置
function unique(arr) {
	// 判断是否为数组
	if (!Array.isArray(arr)) {
		console.log('type error!')
		return
	}
	// 定义新数组
	let newArr = [];
	for (let i = 0; i < arr.length; i++) {
		console.log(arr[i])
		console.log(newArr.indexOf(arr[i]) === -1)
		// 如果新数组有相同的元素则不添加
		if (newArr.indexOf(arr[i]) === -1) {
			newArr.push(arr[i])
			console.log(newArr)
		}
	}
	// 返回新数组
	return newArr
}
let arr = [4, 4, 5, 5, 7]
console.log(unique(arr))

1-3 includes方法

数组的 includes() 方法可判断数组中是否含有此元素

  • 首先定义一个空数组 newArr,然后调用 includes 方法对原来的数组进行遍历判断
  • 如果元素不在新数组中,则将其 push 进新数组中,最后将新数组返回即可获得去重的数组
// 通过includes判断数组是否含有此元素
function unique(arr) {
	// 判断是否为数组
	if (!Array.isArray(arr)) {
		console.log('type error!')
		return
	}
	// 定义新数组
	let newArr = [];
	for (let i = 0; i < arr.length; i++) {
		console.log(arr[i])
		console.log(newArr.indexOf(arr[i]) === -1)
		// 如果新数组有相同的元素则不添加
		if (!newArr.includes(arr[i])) {
			newArr.push(arr[i])
			console.log(newArr)
		}
	}
	// 返回新数组
	return newArr
}
let arr = [4, 4, 5, 5, 7]
console.log(unique(arr))

1-4 fileter + indexOf

利用 indexOf 检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素

// filter 对数组进行迭代过滤
function unique(arr) {
	if (!Array.isArray(arr)) {
		console.log('type error!')
		return
	}
	// 通过Array原型方法返回一个新数组
	return Array.prototype.filter.call(arr, function(item, index) {
		// 判断如果当前元素不等于当前索引则为重复元素
		// 则返回索引与元素相符合的元素组成的新数组
		return arr.indexOf(item) === index;
	})
}
let arr = [2, 2, 4, 5, 7];
console.log(unique(arr));

1-5 相邻元素去重

  • 通过 sort() 方法先对数组进行排序,创建一个新数组
  • 然后根据排序后的结果进行遍历及相邻元素比对,如果相等则跳过该元素,不相等则添加到新数组
// 通过sort()对元素数组排序对相邻的元素进行对比
function unique(arr) {
	if (!Array.isArray(arr)) {
		console.log('type error!')
		return
	}
	arr = arr.sort();
	let newArr = [];
	for (let i = 0; i < arr.length; i++) {
		// 判断相邻的元素是否相同
		console.log(arr[i], arr[i - 1])
		if (arr[i] !== arr[i - 1]) {
			newArr.push(arr[i]);
		}
		console.log(newArr)
	}
	return newArr;
}
let arr = [2, 2, 2, 4, 5, 7];
console.log(unique(arr));

1-6 利用对象属性去重

利用对象属性存在的特性,如果没有该属性则存入新数组

  • 创建空对象,遍历数组,将数组中的值设为对象的属性,并给该属性赋初始值1
  • 每出现一次,对应的属性值增加1,属性值对应的就是该元素出现的次数
// 通过对象属性特性去重
function unique(arr) {
	if (!Array.isArray(arr)) {
		console.log('type error!');
		return
	}
	let newArr = [];
	let obj = {};
	for (let i = 0; i < arr.length; i++) {
		console.log(`i = ${i}`)
		console.log(obj)
		console.log(obj[arr[i]])
		// 对象中无此属性则为不重复元素
		if (!obj[arr[i]]) {
			newArr.push(arr[i]);
			console.log(newArr);
			// 初始对象属性的值
			obj[arr[i]] = 1;
		} else {
			// 如果存在重复属性则对象内属性值+1
			obj[arr[i]]++
		}

	}
	return newArr;
}
let arr = [2, 2, 2, 4, 5, 7];
console.log(unique(arr));

1-7 set与解构赋值去重

Set 是一个特殊的类型集合,Set 里的值不会重复

new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set

function unique(arr) {
	if (!Array.isArray(arr)) {
		console.log('type error!');
		return
	}
	// 通过结构赋值返回新数组
	return [...new Set(arr)]
    return Array.from(new Set(arr))
}
let arr = [1, 1, 4, 4, 4];
console.log(unique(arr));

1-8 reduce数组去重

  • 通过 reduce 方法,先传入起始值为新数组 []
  • 判断新数组中的元素是否含有原始数组的元素,有则返回新数组,没有则通过解构赋值将新数组与原始数组元素合并
function unique(arr) {
    if (!Array.isArray(arr)) {
	    console.log('type error!');
	    return
    }
	return arr.reduce((prev, cur) => {
		console.log(prev)
		console.log(...prev, cur)
		return prev.includes(cur) ? prev : [...prev, cur]
	}, [])
}
let arr = [1, 3, 5, 5]
console.log(unique(arr))

1-9 递归去重

通过递归实现数组去重

  • 通过 sort() 先将原始数组排序
  • 创建一个递归函数,通过数组索引为递归条件,判断相邻元素是否相等,相等则删除,最后返回重复的元素返回元素数组
// 通过递归函数来实现递归
function unique(arr) {
	if (!Array.isArray(arr)) {
		console.log('type error!');
		return
	}
	// 获取原始数组的索引
	let arrLength = arr.length;
	// 将原始数组排序
	arr = arr.sort();

	// 创建递归函数
	/*
	 * @index 索引
	 */
	function loop(index) {
	    // 终止递归的条件
		if (index >= 1) {
			if (arr[index] === arr[index - 1]) {
				arr.splice(index, 1);
			}
			loop(index - 1)
		}
	}
	loop(arrLength - 1)
	return arr
}
let arr = [2, 2, 3, 2, 4];
console.log(unique(arr)); // [2, 3, 4]

2. 数组排序

2-1 冒泡排序

冒泡排序也成为沉淀排序(sinking sort)

冒泡排序是最简单的排序方法,容易理解、实现简单,但是冒泡排序是效率最低的排序算法

  • 它遍历整个数组,将数组的每一项与其后一项进行对比,如果不符合要求就交换位置
  • 一共遍历n轮,n为数组的长度。n轮之后,数组得以完全排序

// 冒泡排序
function bubblesSort(arr) {
    if (!Array.isArray(arr)) return false;
	// 获取数组length 确定循环次数
	let arrLength = arr.length;
	for (let i = 0; i < arrLength; i++) {
		// 遍历数组的前len-i项
		console.log(i)
		for (let j = 0; j < arrLength - 1 - i; j++) {
			//  将每一项与后一项进行对比,不符合要求的就换位
			console.log(arr[j], arr[j + 1])
			if (arr[j] > arr[j + 1]) {
				[arr[j + 1], arr[j]] = [arr[j], arr[j + 1]]
			}
		}
		console.log(arr)

	}
	return arr;
}
let arr = [1, 5, 23, 2]
console.log(bubblesSort(arr))

2-2 选择排序

选择排序,就是反复从未排序的数列中取出最小的元素,加入到另一个数列中,最后的结果即为已排序的数列

从小到大排序的操作是一开始在所有的数据中挑选一个最小项放在第一个位置,再从第二项开始挑选剩下元素的最小项放在第2个位置,以此反复,直到完成排序为止

function selectionSort(arr) {
	if (!Array.isArray(arr)) return false;
	// 获取数组长度
	let len = arr.length;
	for (let i = 0; i < len; i++) {
		// 记录最小索引
		let minIndex = i;
		console.log(minIndex);
		console.log('minIndex-->', arr[minIndex]);
		for (let j = i + 1; j < len; j++) {
			console.log('i-->', i)
			console.log('arr[j]-->', arr[j])
			// 交换位置后原数组的排序会变化,获取的是排序的数组
			minIndex = arr[j] < arr[minIndex] ? j : minIndex
		}
		// 交换位置
		[arr[minIndex], arr[i]] = [arr[i], arr[minIndex]];
		console.log(arr)
	}
	return arr;
}
let arr = [1, 5, 23, 2]
console.log(selectionSort(arr))

2-3 插入排序

  • 从数组的第二项开始遍历数组的n-1项(n为数组长度),遍历过程中对于当前项的左边数组项,依次从右到左进行对比
  • 如果左边选项大于(或小于)当前项,则左边选项向右移动,然后继续对比前一项,直到找到不大于(不小于)自身的选项为止,对于所有大于当前项的选项,都在原来位置的基础上向右移动了一项

// 插入排序
/*
 * @arr 数组类型
 * @return arr
 */
function insertionSort(arr) {
	if (!Array.isArray(arr)) return false;
	for (var i = 0; i < arr.length; i++) {
		//当前元素
		let item = arr[i];
		//前面的索引
		let prevIndex = i - 1;
		// 对前面进行判断
		console.log('prevIndex-->', prevIndex)
		while (prevIndex >= 0 && arr[prevIndex] > item) {
			console.log(arr[prevIndex], item)
			//交换位置
			arr[prevIndex + 1] = arr[prevIndex];
			prevIndex--;
		}
		arr[prevIndex + 1] = item
		console.log(arr)
	}
	return arr;

}
let arr = [60, 30, 5, 23, 80, 2]
console.log(insertionSort(arr))

2-4 归并排序

JavaScript 中的 Array.prototype.sort 方法没有规定使用哪种排序算法,允许浏览器自定义,FireFox 使用的是归并排序法,而 Chrome 使用的是快速排序法

归并排序的核心思想是分治,分治是通过递归地将问题分解成相同或者类型相关的两个或者多个子问题,直到问题简单到足以解决,然后将子问题的解决方案结合起来,解决原始方案的一种思想

将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序

// 归并排序
function mergeSort(arr) {
	if (!Array.isArray(arr)) return false;
	if (arr.length < 2) return arr;
	// 将无序列数组划分为两个数组
	let mid = Math.floor(arr.length / 2);
	let left = arr.slice(0, mid);
	let right = arr.slice(mid, arr.length)
	console.log(left, right)
	// 递归分别对左右两部分数组进行排序合并
	return Merge(this.mergeSort(left), this.mergeSort(right));
}

function Merge(left, right) {
	let result = [];
	while (left.length > 0 && right.length > 0) {
		console.log(left[0], right[0])
		if (left[0] <= right[0]) {
			result.push(left.shift())
		} else {
			result.push(right.shift())
		}
	}
	while (left.length) {
		result.push(left.shift())
	}
	while (right.length) {
		result.push(right.shift())
	}
	console.log('result--> ', result)
	return result
}
let arr = [60, 30, 5, 23, 80, 2]
console.log(mergeSort(arr))

2-5 快速排序

快速排序由Tony Hoare在1959年发明,是当前最为常用的排序方案,如果使用得当,其速度比一般算法可以快两到三倍,比之冒泡排序、选择排序等可以说快成千上万倍

其核心思想也是分而治之,它递归地将大数组分解为小数组,直到数组长度为1,不过与归并排序的区别在于其重点在于数组的分解,而归并排序的重点在于数组的合并

基本思想:

在数组中选取一个参考点 pivot,然后对于数组中的每一项,大于 pivot 的项都放到数组右边,小于 pivot 的项都放到左边,左右两边的数组项可以构成两个新的数组(leftright),然后继续分别对 leftright 进行分解,直到数组长度为1,最后合并返回

基本步骤:

  • (1)首先,选取数组的中间项作为参考点 pivot
  • (2)创建左右两个指针 leftrightleft 指向数组的第一项,right 指向最后一项,然后移动左指针,直到其值不小于 pivot,然后移动右指针,直到其值不大于 pivot
  • (3)如果 left 仍然不大于 right,交换左右指针的值(指针不交换),然后左指针右移,右指针左移,继续循环直到 left 大于 right 才结束,返回 left 指针的值
  • (4)根据上一轮分解的结果(left 的值),切割数组得到 leftright 两个数组,然后分别再分解
  • (5)重复以上过程,直到数组长度为1才结束分解

// 快速排序
function quickSort(arr) {
	if (!Array.isArray(arr)) return false;
	if (arr.length <= 1) return arr;
	let content = parseInt(arr.length / 2);
	// 获取中间值让原数组改变
	let centerItem = arr.splice(content, 1);
	console.log(centerItem)
	let left = [];
	let right = [];
	for (let i = 0; i < arr.length; i++) {
		if (arr[i] <= centerItem) {
			left.push(arr[i])
		} else {
			right.push(arr[i])
		}
		console.log('left -->', left);
		console.log('right -->', right)

	}
    //使用递归 每次取中间排完再调用两边重新排序
	return quickSort(left)
		.concat(centerItem)
		.concat(quickSort(right))
}
let arr = [60, 30, 5, 23, 80, 2]
console.log(quickSort(arr))

2-6 堆排序

堆排序也是一种很高效的排序方法,因为它把数组作为二叉树排序而得名,可以认为是归并排序的改良方案,它是一种原地排序方法

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作大顶堆。 对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆

  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值

  • 从堆结构的最右边的叶子节点开始,从右至左、从下至上依次与根节点进行交换,每次交换后,都要再次构建堆结构。值得注意的是每次构建堆结构时,都要忽略已经交换过的非根节点

// 数组
var arr = [1,2,3,4,5,6,7];
// 堆结构
        1
      /   \
    2       3
  /   \   /   \
4      5 6     7
function heapSort(arr) {
	//console.time('HeapSort');
	buildHeap(arr);
	for (let i = arr.length - 1; i > 0; i--) {
		// 从最右侧的叶子节点开始,依次与根节点的值交换。
		[arr[i], arr[0]] = [arr[0], arr[i]];
		// 每次交换之后都要重新构建堆结构,记得传入i限制范围,防止已经交换的值仍然被重新构建。
		heapify(arr, i, 0);
	}
	//console.timeEnd('HeapSort');
	return arr;

	function buildHeap(arr) {
		// 可以观察到中间下标对应最右边叶子节点的父节点。
		let mid = Math.floor(arr.length / 2);
		for (let i = mid; i >= 0; i--) {
			// 将整个数组构建成堆结构以便初始化。
			heapify(arr, arr.length, i);
		}
		return arr;
	}
	// 从i节点开始下标在heapSize内进行堆结构构建的函数。
	function heapify(arr, heapSize, i) {
		// 左子节点下标。
		let left = 2 * i + 1,
			// 右子节点下标。
			right = 2 * i + 2,
			// 假设当前父节点满足要求(比子节点都大)。
			largest = i;
		// 如果左子节点在heapSize内,并且值大于其父节点,那么left赋给largest。
		if (left < heapSize && arr[left] > arr[largest]) {
			largest = left;
		}
		// 如果右子节点在heapSize内,并且值大于其父节点,那么right赋给largest。
		if (right < heapSize && arr[right] > arr[largest]) {
			largest = right;
		}
		if (largest !== i) {
			// 如果largest被修改了,那么交换两者的值使得构造成一个合格的堆结构。
			[arr[largest], arr[i]] = [arr[i], arr[largest]];
			// 递归调用自身,将节点i所有的子节点都构建成堆结构。
			arguments.callee(arr, heapSize, largest);
		}
		return arr;
	}
}

3. 数组扁平化

3-1 ES6 flat() 方法

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

使用 Infinity,可展开任意深度的嵌套数组

let newArray = arr.flat([depth])

let arr = [[1], 2, [[3], [4]]]
console.log(arr.flat(2)); // [1, 2, 3, 4]

// 使用 Infinity,可展开任意深度的嵌套数组
let arr1 = [[[1], [[[6]], [5]]]]
console.log(arr1.flat(Infinity)); // [1, 6, 5]

3-2 递归 + forEach

使用 forEach 方法自动遍历判断,然后通过递归函数实现扁平化,将数组元素全部遍历出来添加到新数组中

// forEach + 递归 
// forEach 遍历数组会自动跳过空元素
const eachFlat = (arr = [], depth = 1) => {
	const result = []; // 缓存递归结果
	// 开始递归
	(function flat(arr, depth) {
		// forEach 会自动去除数组空位
		arr.forEach((item) => {
			// 控制递归深度
			if (Array.isArray(item) && depth > 0) {
				// 递归数组
				flat(item, depth - 1)
			} else {
				// 缓存元素
				result.push(item)
			}
		})
	})(arr, depth)
	// 返回递归结果
	return result;
}
let arr = [2, [3],[[4]]]
console.log(eachFlat(arr, 2)); // [2, 3, 4]
// for of 循环不能去除数组空位,需要手动去除
const forFlat = (arr = [], depth = 1) => {
  const result = [];
  (function flat(arr, depth) {
    for (let item of arr) {
      if (Array.isArray(item) && depth > 0) {
        flat(item, depth - 1)
      } else {
        // 去除空元素,添加非undefined元素
        item !== void 0 && result.push(item);
      }
    }
  })(arr, depth)
  return result;
}
let arr1 = [2, [3], [[4]]]
console.log(forFlat(arr, 2)); // [2, 3, 4]

3-3 递归 + reduce + concat

使用 reduceconcat 和递归展开无限多层嵌套的数组

通过 reduce 初始化一个新数组,递归函数将多维数组里的值遍历出来,然后使用 concat 方法添加到新数组内

function flatDeep(arr, depth = 1) {
	if (depth > 0) {
		return arr.reduce((prev, cur) => {
			return prev.concat(Array.isArray(cur) ? flatDeep(cur, depth - 1) : cur)
		}, [])
	} else {
		return arr.slice()
	}

}
let arr = [1, [2],[[3]]]
console.log(flatDeep(arr, Infinity)); // [1, 2, 3]

3- 4 …扩展运算符 + some

ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中,所以也可以递归扁平化数组

... 每次只能展开最外层的数组,被 [].concat 后,arr 只扁平化一次

利用 arr.some 判断当数组中还有数组的话,循环利用 [].concat.apply扁平, 用concat 连接,最终返回 arr

// ... + some
function flatDeep(arr) {
	// 通过 some 判断如果是数组则扁平化
	while (arr.some(item => Array.isArray(item))) {
		// 一次只能扁平化一次 通过while循环调用
		arr = [].concat(...arr)
	}
	return arr;
}
let arr = [1, [2],[[3]]]
console.log(flatDeep(arr)); // [1, 2, 3]

3-5 toString + split

利用 toString 把数组变成以逗号分隔的字符串,然后遍历数组把每一项再变回原来的类型

当有空数组时,toString 方法会发生类型转换,将空数组转换为 '' 空字符串

let arr = [1, 2, [67, [2],[], [112]]]
let newArr = arr.toString().split(",")
console.log(newArr); // ['1', '2', '67', '2', '', '112']

3-6 lodash 实现 _.flatMapDepth

_.flatMapDepth(collection, [iteratee=_.identity], [depth=1])

根据指定的 depth(递归深度)扁平化递归映射结果

  • collection (Array|Object): 一个用来迭代遍历的集合
  • [iteratee=_.identity] (Array|Function|Object|string): 每次迭代调用的函数
  • [depth=1] (number): 最大递归深度
// _.flatMap(collection, [iteratee=_.identity])
function duplicate(n) {
	return [n]
}
let arr = [1, [2, [3],[4]]]
console.log(_.flatMapDepth(arr, duplicate, Infinity)); // [1, 2, 3, 4]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值