MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript 泛型函数在数据结构中的应用

2021-11-262.9k 阅读

泛型函数基础概念

在深入探讨泛型函数在数据结构中的应用之前,我们先来回顾一下 TypeScript 中泛型函数的基础概念。泛型函数允许我们在定义函数时,不预先指定某些参数或返回值的具体类型,而是在调用函数时再确定这些类型。这样做的好处是,我们可以编写更加通用、可复用的代码,而无需为每种可能的数据类型都编写一个单独的函数。

定义一个简单的泛型函数,比如 identity 函数,它接受一个参数并返回相同的值:

function identity<T>(arg: T): T {
    return arg;
}

在这个函数定义中,<T> 是类型参数,它代表一个类型占位符。在调用 identity 函数时,可以指定 T 的具体类型,例如:

let result1 = identity<number>(5);
let result2 = identity<string>("hello");

也可以让 TypeScript 自动推断类型参数:

let result3 = identity(10); // TypeScript 自动推断为 number 类型

泛型函数在数组操作中的应用

1. 数组映射(Map)

数组的 map 方法是一个非常常用的数组操作。使用泛型函数,我们可以为 map 方法创建一个更通用的类型定义。假设我们有一个函数 mapArray,它接受一个数组和一个回调函数,并返回一个新的数组,新数组的元素是原数组元素经过回调函数处理后的结果。

function mapArray<T, U>(arr: T[], callback: (item: T) => U): U[] {
    let newArray: U[] = [];
    for (let i = 0; i < arr.length; i++) {
        newArray.push(callback(arr[i]));
    }
    return newArray;
}
// 使用示例
let numbers = [1, 2, 3];
let squaredNumbers = mapArray(numbers, (num) => num * num);
console.log(squaredNumbers); // 输出: [1, 4, 9]

let strings = ["1", "2", "3"];
let parsedNumbers = mapArray(strings, (str) => parseInt(str));
console.log(parsedNumbers); // 输出: [1, 2, 3]

在上述代码中,mapArray 函数使用了两个类型参数 TUT 代表输入数组元素的类型,U 代表回调函数返回值的类型,也就是输出数组元素的类型。通过这种方式,我们可以处理不同类型数组的映射操作,而不需要为每种类型分别编写映射函数。

2. 数组过滤(Filter)

数组的 filter 方法用于从数组中筛选出符合条件的元素。同样地,我们可以使用泛型函数来实现一个通用的过滤函数。

function filterArray<T>(arr: T[], callback: (item: T) => boolean): T[] {
    let newArray: T[] = [];
    for (let i = 0; i < arr.length; i++) {
        if (callback(arr[i])) {
            newArray.push(arr[i]);
        }
    }
    return newArray;
}
// 使用示例
let allNumbers = [1, 2, 3, 4, 5];
let evenNumbers = filterArray(allNumbers, (num) => num % 2 === 0);
console.log(evenNumbers); // 输出: [2, 4]

let words = ["apple", "banana", "cherry"];
let shortWords = filterArray(words, (word) => word.length < 6);
console.log(shortWords); // 输出: ["apple", "cherry"]

filterArray 函数只使用了一个类型参数 T,表示数组元素的类型。回调函数接受一个 T 类型的元素,并返回一个布尔值,用于判断该元素是否应该被保留在新数组中。

泛型函数在链表数据结构中的应用

链表是一种常见的数据结构,它由一系列节点组成,每个节点包含一个数据和指向下一个节点的引用(在双向链表中还包含指向前一个节点的引用)。使用泛型函数可以使链表的操作更加通用,能够适应不同数据类型的链表。

1. 单链表节点定义

首先,我们定义单链表的节点:

class ListNode<T> {
    value: T;
    next: ListNode<T> | null;
    constructor(value: T) {
        this.value = value;
        this.next = null;
    }
}

这里,ListNode 类使用了泛型 T,表示节点中存储的数据类型。

2. 单链表添加节点操作

接下来,我们实现一个向单链表添加节点的泛型函数:

function addNode<T>(head: ListNode<T> | null, value: T): ListNode<T> {
    let newNode = new ListNode<T>(value);
    if (!head) {
        return newNode;
    }
    let current = head;
    while (current.next) {
        current = current.next;
    }
    current.next = newNode;
    return head;
}
// 使用示例
let listHead: ListNode<number> | null = null;
listHead = addNode(listHead, 1);
listHead = addNode(listHead, 2);
listHead = addNode(listHead, 3);

addNode 函数中,我们使用了泛型 T 来确保传入的值和节点存储的数据类型一致。无论链表存储的是数字、字符串还是其他类型的数据,都可以使用这个函数来添加节点。

3. 单链表遍历操作

再来看单链表的遍历操作,我们编写一个泛型函数来遍历链表并对每个节点的值执行特定操作:

function traverseList<T>(head: ListNode<T> | null, callback: (value: T) => void) {
    let current = head;
    while (current) {
        callback(current.value);
        current = current.next;
    }
}
// 使用示例
traverseList(listHead, (value) => console.log(value)); 
// 输出: 1 2 3

traverseList 函数接受链表头节点和一个回调函数,回调函数接受节点的值并执行相应操作。由于使用了泛型 T,这个函数可以适用于任何类型数据的链表遍历。

泛型函数在栈数据结构中的应用

栈是一种后进先出(LIFO)的数据结构。我们可以使用泛型函数来实现一个通用的栈操作集合。

1. 栈的基本操作定义

首先定义栈的基本结构和操作函数:

class Stack<T> {
    private items: T[];
    constructor() {
        this.items = [];
    }
    push(item: T): void {
        this.items.push(item);
    }
    pop(): T | undefined {
        return this.items.pop();
    }
    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }
    isEmpty(): boolean {
        return this.items.length === 0;
    }
    size(): number {
        return this.items.length;
    }
}

在这个 Stack 类中,使用泛型 T 表示栈中存储的数据类型。push 方法用于将元素压入栈,pop 方法用于弹出栈顶元素,peek 方法用于查看栈顶元素,isEmpty 方法用于判断栈是否为空,size 方法用于获取栈的大小。

2. 栈操作的泛型辅助函数

有时候,我们可能需要一些辅助函数来操作栈。例如,一个将栈中所有元素乘以某个因子的函数:

function multiplyStackElements<T extends number>(stack: Stack<T>, factor: number): void {
    let newItems: T[] = [];
    while (!stack.isEmpty()) {
        let item = stack.pop();
        if (item!== undefined) {
            newItems.unshift(item * factor as T);
        }
    }
    newItems.forEach((item) => stack.push(item));
}
// 使用示例
let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
multiplyStackElements(numberStack, 2);
let resultStack: number[] = [];
while (!numberStack.isEmpty()) {
    let item = numberStack.pop();
    if (item!== undefined) {
        resultStack.unshift(item);
    }
}
console.log(resultStack); // 输出: [6, 4, 2]

multiplyStackElements 函数中,通过 T extends number 限定了泛型 T 必须是 number 类型。这样可以确保我们对栈中元素执行乘法操作是安全的,因为只有数字类型才能进行乘法运算。

泛型函数在队列数据结构中的应用

队列是一种先进先出(FIFO)的数据结构。同样地,我们可以利用泛型函数来实现通用的队列操作。

1. 队列的基本操作定义

class Queue<T> {
    private items: T[];
    constructor() {
        this.items = [];
    }
    enqueue(item: T): void {
        this.items.push(item);
    }
    dequeue(): T | undefined {
        return this.items.shift();
    }
    front(): T | undefined {
        return this.items[0];
    }
    isEmpty(): boolean {
        return this.items.length === 0;
    }
    size(): number {
        return this.items.length;
    }
}

Queue 类使用泛型 T 来表示队列中存储的数据类型。enqueue 方法用于将元素加入队列,dequeue 方法用于从队列头部移除元素,front 方法用于查看队列头部元素,isEmpty 方法用于判断队列是否为空,size 方法用于获取队列的大小。

2. 队列操作的泛型转换函数

假设我们有一个需求,将队列中的所有元素转换为另一种类型。例如,将字符串队列中的所有字符串转换为数字(假设字符串都能合法转换为数字):

function convertQueue<T, U>(queue: Queue<T>, converter: (item: T) => U): Queue<U> {
    let newQueue = new Queue<U>();
    while (!queue.isEmpty()) {
        let item = queue.dequeue();
        if (item!== undefined) {
            newQueue.enqueue(converter(item));
        }
    }
    return newQueue;
}
// 使用示例
let stringQueue = new Queue<string>();
stringQueue.enqueue("1");
stringQueue.enqueue("2");
stringQueue.enqueue("3");
let numberQueue = convertQueue(stringQueue, (str) => parseInt(str));
let resultQueue: number[] = [];
while (!numberQueue.isEmpty()) {
    let item = numberQueue.dequeue();
    if (item!== undefined) {
        resultQueue.push(item);
    }
}
console.log(resultQueue); // 输出: [1, 2, 3]

convertQueue 函数使用了两个泛型 TUT 表示原队列元素的类型,U 表示转换后队列元素的类型。通过传入的转换函数 converter,实现了队列元素类型的转换。

泛型函数在树数据结构中的应用

树是一种层次结构的数据结构,常见的有二叉树、二叉搜索树等。泛型函数在树的操作中也有着广泛的应用。

1. 二叉树节点定义

class TreeNode<T> {
    value: T;
    left: TreeNode<T> | null;
    right: TreeNode<T> | null;
    constructor(value: T) {
        this.value = value;
        this.left = null;
        this.right = null;
    }
}

TreeNode 类使用泛型 T 来表示节点存储的数据类型。

2. 二叉树遍历操作

二叉树有多种遍历方式,如前序遍历、中序遍历和后序遍历。以中序遍历为例,我们可以编写一个泛型函数:

function inorderTraversal<T>(root: TreeNode<T> | null, callback: (value: T) => void) {
    if (root) {
        inorderTraversal(root.left, callback);
        callback(root.value);
        inorderTraversal(root.right, callback);
    }
}
// 使用示例
let root: TreeNode<number> = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
inorderTraversal(root, (value) => console.log(value)); 
// 输出: 2 1 3

inorderTraversal 函数接受二叉树的根节点和一个回调函数。通过递归方式进行中序遍历,并对每个节点的值执行回调函数。由于使用了泛型 T,这个函数可以适用于任何类型数据的二叉树中序遍历。

3. 二叉搜索树插入操作

二叉搜索树是一种特殊的二叉树,左子树的所有节点值小于根节点值,右子树的所有节点值大于根节点值。我们可以编写一个泛型函数来插入节点到二叉搜索树中:

function insertIntoBST<T extends number>(root: TreeNode<T> | null, value: T): TreeNode<T> {
    if (!root) {
        return new TreeNode<T>(value);
    }
    if (value < root.value) {
        root.left = insertIntoBST(root.left, value);
    } else {
        root.right = insertIntoBST(root.right, value);
    }
    return root;
}
// 使用示例
let bstRoot: TreeNode<number> | null = null;
bstRoot = insertIntoBST(bstRoot, 5);
bstRoot = insertIntoBST(bstRoot, 3);
bstRoot = insertIntoBST(bstRoot, 7);

insertIntoBST 函数中,通过 T extends number 限定了泛型 T 必须是 number 类型,因为二叉搜索树的比较操作通常基于数字类型。这个函数递归地找到合适的位置插入新节点,保持二叉搜索树的性质。

泛型函数在哈希表数据结构中的应用

哈希表是一种以键值对形式存储数据的数据结构,通过哈希函数将键映射到一个哈希值,从而快速定位对应的值。泛型函数可以帮助我们实现一个通用的哈希表操作集合。

1. 哈希表基本操作定义

class HashTable<K, V> {
    private table: { [key: string]: V };
    constructor() {
        this.table = {};
    }
    set(key: K, value: V): void {
        let stringKey = String(key);
        this.table[stringKey] = value;
    }
    get(key: K): V | undefined {
        let stringKey = String(key);
        return this.table[stringKey];
    }
    has(key: K): boolean {
        let stringKey = String(key);
        return this.table.hasOwnProperty(stringKey);
    }
    delete(key: K): boolean {
        let stringKey = String(key);
        if (this.table.hasOwnProperty(stringKey)) {
            delete this.table[stringKey];
            return true;
        }
        return false;
    }
}

HashTable 类中,使用了两个泛型 KVK 表示键的类型,V 表示值的类型。set 方法用于设置键值对,get 方法用于获取指定键的值,has 方法用于判断哈希表中是否存在指定键,delete 方法用于删除指定键值对。

2. 哈希表操作的泛型转换函数

例如,我们可能需要将哈希表中所有的值转换为另一种类型。假设我们有一个哈希表存储数字,现在要将所有数字乘以 2:

function transformHashTableValues<K, V extends number, U extends number>(hashTable: HashTable<K, V>, transformer: (value: V) => U): HashTable<K, U> {
    let newHashTable = new HashTable<K, U>();
    for (let key in hashTable.table) {
        if (hashTable.table.hasOwnProperty(key)) {
            let value = hashTable.table[key];
            newHashTable.set(key as K, transformer(value));
        }
    }
    return newHashTable;
}
// 使用示例
let numberHashTable = new HashTable<string, number>();
numberHashTable.set("a", 1);
numberHashTable.set("b", 2);
let newHashTable = transformHashTableValues(numberHashTable, (num) => num * 2);
console.log(newHashTable.get("a")); // 输出: 2
console.log(newHashTable.get("b")); // 输出: 4

transformHashTableValues 函数使用了三个泛型 KVUK 表示键的类型,V 表示原哈希表值的类型,U 表示转换后哈希表值的类型。通过传入的转换函数 transformer,实现了哈希表值类型的转换。

泛型函数在图数据结构中的应用

图是一种复杂的数据结构,由节点和边组成。泛型函数可以帮助我们实现通用的图操作。

1. 图节点定义

class GraphNode<T> {
    value: T;
    neighbors: GraphNode<T>[];
    constructor(value: T) {
        this.value = value;
        this.neighbors = [];
    }
}

GraphNode 类使用泛型 T 表示节点存储的数据类型。每个节点都有一个邻居节点数组。

2. 图的广度优先搜索(BFS)

广度优先搜索是一种图的遍历算法,从给定的起始节点开始,逐层遍历图中的节点。

function bfs<T>(start: GraphNode<T>, callback: (node: GraphNode<T>) => void) {
    let queue: GraphNode<T>[] = [];
    let visited: { [key: string]: boolean } = {};
    queue.push(start);
    visited[String(start.value)] = true;
    while (queue.length > 0) {
        let current = queue.shift();
        if (current) {
            callback(current);
            for (let neighbor of current.neighbors) {
                if (!visited[String(neighbor.value)]) {
                    queue.push(neighbor);
                    visited[String(neighbor.value)] = true;
                }
            }
        }
    }
}
// 使用示例
let node1 = new GraphNode(1);
let node2 = new GraphNode(2);
let node3 = new GraphNode(3);
node1.neighbors.push(node2);
node2.neighbors.push(node3);
bfs(node1, (node) => console.log(node.value)); 
// 输出: 1 2 3

bfs 函数使用泛型 T 来适应不同类型数据的图节点。通过队列和访问记录来实现广度优先搜索,并对每个访问到的节点执行回调函数。

3. 图的深度优先搜索(DFS)

深度优先搜索也是一种图的遍历算法,从给定的起始节点开始,尽可能深地探索每个分支,直到无法继续或达到目标节点。

function dfs<T>(start: GraphNode<T>, callback: (node: GraphNode<T>) => void) {
    let visited: { [key: string]: boolean } = {};
    function dfsHelper(node: GraphNode<T>) {
        if (visited[String(node.value)]) {
            return;
        }
        visited[String(node.value)] = true;
        callback(node);
        for (let neighbor of node.neighbors) {
            dfsHelper(neighbor);
        }
    }
    dfsHelper(start);
}
// 使用示例
dfs(node1, (node) => console.log(node.value)); 
// 输出: 1 2 3

dfs 函数同样使用泛型 T,通过递归和访问记录来实现深度优先搜索,并对每个访问到的节点执行回调函数。

泛型函数在数据结构应用中的注意事项

  1. 类型约束:在使用泛型函数时,要注意合理使用类型约束。例如,在对数值进行特定操作时,需要通过 T extends number 等方式限制泛型类型,以确保类型安全。如果不进行适当约束,可能会在运行时出现类型错误。
  2. 性能影响:虽然泛型函数提供了很高的代码复用性,但在某些情况下,可能会对性能产生一定影响。例如,在频繁操作大数据量的情况下,泛型函数的类型检查和转换可能会带来额外的开销。此时,可能需要考虑针对特定类型进行优化,编写非泛型版本的函数。
  3. 可读性:尽管泛型函数可以使代码更通用,但过多复杂的泛型使用可能会降低代码的可读性。在编写泛型函数时,要确保代码结构清晰,类型参数命名合理,尽量减少不必要的泛型嵌套,以便其他开发人员能够容易理解和维护代码。

通过以上对泛型函数在各种数据结构中的应用介绍,我们可以看到泛型函数在提高代码复用性、灵活性和类型安全性方面具有显著优势。在实际前端开发中,合理运用泛型函数能够大大提升开发效率,减少重复代码,使代码更加健壮和易于维护。无论是简单的数组操作,还是复杂的图数据结构,泛型函数都能为我们提供强大的支持。