数据结构实战指南从数组链表到树的性能优化技巧

迷人的文亭 优化 阅读 2,699
赞 21 收藏
二维码
手机扫码查看
反馈

先说说我为啥要重新整理数据结构

最近重构一个老项目的搜索功能,用户输入关键词后要返回相关商品列表,但数据量特别大,每次搜索都卡得不行。看了眼控制台,原来是从后端返回了一个复杂的嵌套对象,前端还要做各种过滤和排序处理。折腾了半天,发现根本问题还是数据结构设计得太乱了。

数据结构实战指南从数组链表到树的性能优化技巧

以前总是想着能跑就行,现在数据量上来了,之前那种随意组织数据的方式真的要命。所以这次好好整理了下常用的数据结构优化技巧,亲测有效。

数组转Map,查找效率直接起飞

这是我在项目中最常用的优化手段。之前有个需求是要根据用户ID快速找到对应信息,在一个包含上千个用户的数组里反复查找,性能差得很。

优化前的代码长这样:

// 低效查找 - O(n) 时间复杂度
function getUserById(userId) {
    return users.find(user => user.id === userId);
}

// 每次查找都要遍历整个数组,太慢了
const targetUser = getUserById(targetId);

改成Map之后,查找变成了O(1)时间复杂度:

// 将数组转换为Map
const userMap = new Map();
users.forEach(user => {
    userMap.set(user.id, user);
});

// 高效查找 - O(1) 时间复杂度
function getUserById(userId) {
    return userMap.get(userId);
}

这里踩了个坑,就是Map的key如果是字符串的话,要注意类型匹配问题。比如用户ID是数字,但是有时候前端传过来变成字符串,这时候就要注意统一类型。

扁平化处理,减少嵌套层级

后端返回的数据经常是一层层的嵌套,比如电商项目里的分类数据:

const nestedCategories = [
    {
        id: 1,
        name: "电子产品",
        children: [
            {
                id: 11,
                name: "手机",
                children: [
                    { id: 111, name: "iPhone" },
                    { id: 112, name: "Android" }
                ]
            },
            {
                id: 12,
                name: "电脑",
                children: [
                    { id: 121, name: "笔记本" },
                    { id: 122, name: "台式机" }
                ]
            }
        ]
    }
];

这种嵌套层级很深的数据处理起来很麻烦,所以我习惯用递归转成一维数组:

function flattenTree(categories, parentPath = []) {
    let result = [];
    
    categories.forEach(category => {
        const currentPath = [...parentPath, category.name];
        
        // 添加当前节点
        result.push({
            id: category.id,
            name: category.name,
            path: currentPath.join(' > '),
            level: currentPath.length - 1
        });
        
        // 递归处理子节点
        if (category.children && category.children.length) {
            result = result.concat(flattenTree(category.children, currentPath));
        }
    });
    
    return result;
}

const flatCategories = flattenTree(nestedCategories);
// 输出:
// [
//   {id: 1, name: "电子产品", path: "电子产品", level: 0},
//   {id: 11, name: "手机", path: "电子产品 > 手机", level: 1},
//   {id: 111, name: "iPhone", path: "电子产品 > 手机 > iPhone", level: 2}
//   ...
// ]

这样处理后,查找、筛选、渲染都变得很简单,特别是需要显示面包屑导航的时候特别方便。

索引映射,避免重复计算

在处理大量商品数据时,经常需要根据多个维度来筛选,比如价格区间、品牌、类别等。如果每次都重新计算就会很慢。

我的解决方案是建立索引映射:

class ProductIndexer {
    constructor(products) {
        this.products = products;
        this.priceIndex = new Map(); // 价格索引
        this.brandIndex = new Map(); // 品牌索引
        this.categoryIndex = new Map(); // 分类索引
        
        this.buildIndexes();
    }
    
    buildIndexes() {
        this.products.forEach((product, index) => {
            // 构建价格区间索引
            const priceRange = Math.floor(product.price / 100) * 100; // 每100元一个区间
            if (!this.priceIndex.has(priceRange)) {
                this.priceIndex.set(priceRange, []);
            }
            this.priceIndex.get(priceRange).push(index);
            
            // 构建品牌索引
            if (!this.brandIndex.has(product.brand)) {
                this.brandIndex.set(product.brand, []);
            }
            this.brandIndex.get(product.brand).push(index);
            
            // 构建分类索引
            if (!this.categoryIndex.has(product.category)) {
                this.categoryIndex.set(product.category, []);
            }
            this.categoryIndex.get(product.category).push(index);
        });
    }
    
    filterByPrice(min, max) {
        const result = [];
        for (let [range, indices] of this.priceIndex) {
            if (range >= min && range <= max) {
                result.push(...indices);
            }
        }
        return [...new Set(result)].map(i => this.products[i]);
    }
    
    filterByBrand(brand) {
        const indices = this.brandIndex.get(brand) || [];
        return indices.map(i => this.products[i]);
    }
    
    multiFilter(options = {}) {
        let result = new Set();
        
        if (options.minPrice !== undefined && options.maxPrice !== undefined) {
            const priceFiltered = new Set(
                this.filterByPrice(options.minPrice, options.maxPrice)
                    .map(p => p.id)
            );
            if (result.size === 0) {
                result = priceFiltered;
            } else {
                result = new Set([...result].filter(x => priceFiltered.has(x)));
            }
        }
        
        if (options.brand) {
            const brandFiltered = new Set(
                this.filterByBrand(options.brand)
                    .map(p => p.id)
            );
            if (result.size === 0) {
                result = brandFiltered;
            } else {
                result = new Set([...result].filter(x => brandFiltered.has(x)));
            }
        }
        
        return Array.from(result).map(id => 
            this.products.find(p => p.id === id)
        );
    }
}

踩坑提醒:这种预处理方式适合数据不经常变化的场景。如果数据频繁更新,维护索引的成本可能比直接查询还高。而且要特别注意内存占用,大量数据的索引也会占用不少空间。

缓存策略,避免重复请求

数据结构优化不仅仅是内存里的事情,API层面也需要考虑缓存。我在jztheme.com的项目中就遇到过这个问题,同一个用户信息被反复请求。

class DataCache {
    constructor(ttl = 300000) { // 默认5分钟过期
        this.cache = new Map();
        this.ttl = ttl;
    }
    
    set(key, value) {
        this.cache.set(key, {
            data: value,
            timestamp: Date.now()
        });
    }
    
    get(key) {
        const item = this.cache.get(key);
        if (!item) return null;
        
        if (Date.now() - item.timestamp > this.ttl) {
            this.cache.delete(key); // 过期删除
            return null;
        }
        
        return item.data;
    }
    
    async getData(key, fetchFunction) {
        const cached = this.get(key);
        if (cached) {
            return cached;
        }
        
        const data = await fetchFunction();
        this.set(key, data);
        return data;
    }
}

// 使用示例
const cache = new DataCache();

async function getUserInfo(userId) {
    return await cache.getData(user_${userId}, async () => {
        const response = await fetch(https://jztheme.com/api/users/${userId});
        return response.json();
    });
}

这里的关键是设置合适的TTL时间,太短了缓存效果不好,太长了数据可能不准确。一般根据业务特点来调整,比如用户基本信息可以设置较长的缓存时间,订单状态这种实时性要求高的就要短一点。

内存监控,别让优化变成负担

说了这么多优化技巧,但别忘了监控内存使用情况。数据结构优化后虽然提高了查询效率,但也可能增加了内存占用。

我用过这种方式监控内存:

function monitorMemory() {
    if (performance.memory) {
        console.log('内存使用情况:', {
            used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + 'MB',
            total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + 'MB',
            limit: Math.round(performance.memory.jsHeapSizeLimit / 1024 / 1024) + 'MB'
        });
    }
}

// 定期检查
setInterval(monitorMemory, 30000);

如果发现内存持续增长,要考虑清理不必要的缓存,或者限制缓存大小。

小结

数据结构优化确实能显著提升性能,但这只是冰山一角。选择合适的数据结构要考虑具体场景,比如数据量大小、读写频率、更新频率等等。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论