cad.net 排序和搜索

基础

众所周知,我们很经常找到一个目标,然后中断搜索,

传统方式:

var array = int[] { 1,5,6,3,2,8,9,6,5,4 };
var target = 8;
int i;
for(i = 0; i < array.Length; i++) {
    if (array[i] == target)
       break; 
}
Debug.WriteLine($"目标索引: {i}");

下面有几个搜索并返回的方法:
1: list.FindIndex会找到第一个索引位置,等价传统方式.
2: list.Find是list专用的,性能比3高一点,找到返回对象,找不到:class返回null,struct返回default.
3: list.FirstOrDefault是迭代器用的,找到返回对象,找不到:class返回null,struct返回default.
4: list.First找到第一个,找不到会报错,最好不要用它咯.
5: list.LastOrDefault 找到最后一个满足,API对称设计,IList是倒序遍历,迭代器需要遍历全部.

当这个数组很长的时候,你会发现这种排序之后再搜索,可以快6倍!
原理就是CPU分支预测功能可以对其加速.
但是,我们已经排序了,那么为什么不尝试二分法呢?

int result = Array.BinarySearch(array, target);
if (result >= 0) {
    Console.WriteLine($"目标值 {target} 在数组中的索引是 {result}");
} else {
    Console.WriteLine($"目标值 {target} 不在数组中.查找结果为 {result},该值表示如果要插入元素应插入的位置(取反减1)");
}

写个main测试

using System;
using System.Collections.Generic;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        const long TicksPerNanosecond = 10;

        var list = new List<int>();
        Random random = new Random();
        for (int j = 0; j < 100000; j++) list.Add(random.Next(1, 1000000));
        var target = list[random.Next(0, list.Count)];

        Stopwatch stopwatch1 = Stopwatch.StartNew();
        int i;
        for (i = 0; i < list.Count; i++)
            if (list[i] == target) break;
        stopwatch1.Stop();
        long elapsedNs1 = stopwatch1.ElapsedTicks / TicksPerNanosecond;
        Console.WriteLine($"传统for方式耗时: {elapsedNs1} 纳秒,目标值: {list[i]},目标索引: {i}");

        Stopwatch stopwatch2a = Stopwatch.StartNew();
        int findIndex = list.FindIndex(element => element == target);
        stopwatch2a.Stop();
        long elapsedNs2a = stopwatch2a.ElapsedTicks / TicksPerNanosecond;
        Console.WriteLine($"排序前FindIndex方法耗时: {elapsedNs2a} 纳秒,目标值: {list[findIndex]},目标索引: {findIndex}");

        Stopwatch stopwatch2 = Stopwatch.StartNew();
        var findResult = list.Find(element => element == target);
        stopwatch2.Stop();
        long elapsedNs2 = stopwatch2.ElapsedTicks / TicksPerNanosecond;
        Console.WriteLine($"排序前Find方法耗时: {elapsedNs2} 纳秒,目标值: {findResult}");

        // 新副本并排序
        var sortedList = new List<int>(list);
        sortedList.Sort();

        Stopwatch stopwatch3 = Stopwatch.StartNew();
        var sortedFindResult = sortedList.Find(element => element == target);
        stopwatch3.Stop();
        long elapsedNs3 = stopwatch3.ElapsedTicks / TicksPerNanosecond;
        Console.WriteLine($"排序后Find方法耗时: {elapsedNs3} 纳秒,目标值: {sortedFindResult}");

        Stopwatch stopwatch4 = Stopwatch.StartNew();
        int binarySearchResult = sortedList.BinarySearch(target);
        stopwatch4.Stop();
        long elapsedNs4 = stopwatch4.ElapsedTicks / TicksPerNanosecond;
        if (binarySearchResult >= 0) 
            Console.WriteLine($"二分查找耗时: {elapsedNs4} 纳秒,目标值 {target} 在数组中的索引是 {binarySearchResult}");
        else Console.WriteLine($"二分查找耗时: {elapsedNs4} 纳秒,目标值 {target} 不在数组中.查找结果为 {binarySearchResult},该值表示如果要插入元素应插入的位置(取反减1)");
    }
}

测试结果

测试版本net8.0 release模式 控制台:

第一次跑:
传统for方式耗时: 5 纳秒,目标值: 103802,目标索引: 10134
排序前FindIndex方法耗时: 40 纳秒,目标值: 103802,目标索引: 10134
排序前Find方法耗时: 23 纳秒,目标值: 103802
排序后Find方法耗时: 34 纳秒,目标值: 103802
二分查找耗时: 8 纳秒,目标值 103802 在数组中的索引是 10366

第二次跑:
传统for方式耗时: 37 纳秒,目标值: 553015,目标索引: 84828
排序前FindIndex方法耗时: 124 纳秒,目标值: 553015,目标索引: 84828
排序前Find方法耗时: 110 纳秒,目标值: 553015
排序后Find方法耗时: 343 纳秒,目标值: 553015
二分查找耗时: 23 纳秒,目标值 553015 在数组中的索引是 55200

换一个人的电脑跑,
netnet4.8 release模式 控制台 改成一百万.

第一次跑:
传统for方式耗时: 234 纳秒,目标值: 900769,目标索引: 49318
排序前FindIndex方法耗时: 225 纳秒,目标值: 900769,目标索引: 49318
排序前Find方法耗时: 158 纳秒,目标值: 900769
排序后Find方法耗时: 341 纳秒,目标值: 900769
二分查找耗时: 48 纳秒,目标值 900769 在数组中的索引是 90171

第二次跑:
传统for方式耗时: 49 纳秒,目标值: 383880,目标索引: 10351
排序前FindIndex方法耗时: 104 纳秒,目标值: 383880,目标索引: 10351
排序前Find方法耗时: 64 纳秒,目标值: 383880
排序后Find方法耗时: 129 纳秒,目标值: 383880
二分查找耗时: 38 纳秒,目标值 383880 在数组中的索引是 38494

第三次跑:
传统for方式耗时: 262 纳秒,目标值: 708638,目标索引: 54940
排序前FindIndex方法耗时: 226 纳秒,目标值: 708638,目标索引: 54940
排序前Find方法耗时: 166 纳秒,目标值: 708638
排序后Find方法耗时: 244 纳秒,目标值: 708638
二分查找耗时: 49 纳秒,目标值 708638 在数组中的索引是 71069

结论

从平均期望来说,二分法还是牛逼.

LINQ排序前后的差距不大,只有3倍.
我没有测试传统for的排序前后,大概是6倍,因为网上有人测试过..

NET8.0的传统for和FindIndex居然有些许差距是怎么肥四?
被拒绝内联了吗?
还是委托链的延迟绑定函数其实无法展开?
委托要命中委托链,再查询虚函数表,然后才能命中函数再调用.
多次之后才能特化重写.

二分法必须要排序再用.那么排序耗时不记录吗?
很多时候是允许排序耗时的,因为排序一次,搜索n次.
所以不要在乎这点时间,要在乎搜索的单次时间.

之所以这么死扣性能,
主要是因为扫描线算法还真是log2(n)*2*100w
http://bbs.mjtd.com/thread-192041-1-1.html

posted @ 2025-01-23 16:34  惊惊  阅读(77)  评论(0)    收藏  举报