算法_1:由浅入深学习快速排序QuickSort

目录

一、快排原理

二、单指针快排

[2.1 划分原理](#2.1 划分原理)

[2.2 性能表现](#2.2 性能表现)

2.3总结

三、随机化取枢轴

3.1划分原理

3.2性能表现

3.3总结

四、三路快排

4.1划分原理

4.2性能表现

4.3总结

五、混合快排

5.1划分原理

5.2性能表现

5.3总结

六、实战

七、代码

在众多排序算法中,例如我们熟知的冒泡排序、归并排序,插入排序、基数排序、堆排序、基数排序等等,他们各有适用场景。

但很多编程语言的内置排序算法仍然选择快速排序作为底层核心零件。

我们知道,**快速排序在数组有序的情况下(包含升序、逆序、等值数组)其时间复杂度会从退化成。**只有我们加以改进快排,才有可能规避退化的风险。

因此我们需要先从快速排序的实现原理学习,探讨为什么会退化,以及给出一些现有的成熟的方案。

一、快排原理

给定一个数组numsN = {x1,x2,x3,x4,...,xn},定义一个可以递归的快速排序算法quickSort(int nums\[\],p,r).

需要说明的是,函数第一个形参是待排序数组首地址,第二个、第三个形参是待排序数组的区间。也就是说,quickSort调用一次,只会对p,r范围内的元素操作。

那么容易知道,为了实现对nums\[\]数组的排序,我们在初次调用时应这样调用:quickSort(nums,0,nums.length-1).

核心:每次调用quickSort时,只负责把arrp 放到区间为p+1,r的合适位置q处,使得p...q-1这些数都小于等于arrp,同时q+1...r这些数都大于arrp.

这里的arrp起到一个类似中枢的作用,把数组划分成了两部分,记这个中枢为pivot.

则划分后有:

也即把数组区间首元素放到一个合适的位置,使得左边都小于等于它,右边都大于它。

得到这样的q后,我们只需要递归地处理p...q-1片段里的元素,和q+1...r里的元素。

只要p >= r则递归结束,这就是递归出口,于是得到如下的函数:

cpp 复制代码
void quickSort(int arr[],int p, int r) {
	if(r <= p) {
		return ;
	}
	// 得到边界点
	int q = partition(arr,p,r);
	// 递归左区间
	quickSort(arr,p,point-1);
	// 递归右区间
	quickSort(arr,point+1,r);
}

现在我们关心,如何划分才能得到q,且不同的划分方式会影响算法的性能。

二、单指针快排

我们可以考虑这样一个划分方法。

我们使用单指针i始终维护一个"小数区间",使得p+1,i)之间的数都小于等于pivot.这个很容易做到,从前往后遍历\[p+1,r,只要arrj<= pivot则将其交换至arri的位置,这样下来,就可以保证[p+1,r)之间的数都小于等于pivot.

当遍历完成时,i-1的位置是arrp的合适位置,最后交换arrp和arri-1即可。

cpp 复制代码
int partition(int arr[],int p, int r) {
	// 维护一个"小数字区间边界minIndex"保证minIndex前面的数都是小于等于pivot的
	int minIndex = p + 1;
	// 保存中枢
	int pivot = arr[p];
	int i = p+1;
	for(i = p+1; i <= r; i++) {
		if(arr[i] <= pivot) {
			// 放入小数字区后面 也就是说让arr[minIndex] 和 arr[i] 交换
			int temp = arr[minIndex];
			arr[minIndex] = arr[i];
			arr[i] = temp;
			// 小数字区向右延申
			minIndex ++;
		}
		// 考虑一件事情,是否会把和Pivot相等数字交换到右边去呢?
		// 答案是不会的,因为我们前面写了arr[i] <= pivot则保证了不会把和pivot相等的元素交换到后面去
	}
	// 此时小数字区末尾可能会越界,所以保险起见返回minIndex - 1
	// 最后交换pivot和minIndex - 1
	arr[p] = arr[minIndex - 1];
	arr[minIndex - 1] = pivot;
	return minIndex - 1;
}

2.1 划分原理

下面我们用三个数组来演示这种划分情况下的执行流程。

nums1\[\] = {2,3,5,4,1} nums2\[\] = {1,2,3,4,5} nums3\[\] = {5,4,3,2,1} nums4\[\] = {1,1,1,1,1}

再看看nums4

比较和交换的次数非常多,都是O(N^2)

其他的就不画了,我们可以总结一下。

|------|-----------|-----------|
| 数组类型 | 比较次数 | 交换次数 |
| 随机数组 | O(NlongN) | O(NlongN) |
| 升序数组 | O(N^2) | O(N) |
| 降序数组 | O(N^2) | O(2N) |
| 相等数组 | O(N^2) | O(N^2) |

于是我们便得出初步结论,随机数组 优于 升序数组 优于 降序数组 优于 相等数组

2.2 性能表现

我们需要测试这种划分方式在面对随机数组、升序数组、降序数组、等值数组时的排序性能表现。

对此提前准备好测试函数是十分有必要的。(rand()函数在<stdlib.h>中用于取随机数,clock()在<time.h>中用于记录时间)

cpp 复制代码
// 生成N容量的随机数组
void gen_random(int a[]) {
	int i = 0;
	for(i = 0; i < N; i ++) {
		a[i] = rand() % 1000;
	}
}

// 生成N容量的升序数组
void gen_asc(int a[]) {
	for(int i=0;i<N;i++){
		a[i] = i;
	}
}

// 生成N容量的降序数组
void gen_desc(int a[]) {
	for(int i=0;i<N;i++){
		a[i] = N-1 - i;
	}
}

//生成N容量的一致数组
void gen_same(int a[]) {
	for(int i=0;i<N;i++){
		a[i] = 5;
	}
}

// 测试1.随机数组
void test_random(int a[]) {
	gen_random(a);
	clock_t start,end;
	start = clock();
	quickSort(a,0,N-1);
	end = clock();
	printf("随机数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}

// 测试2.升序数组
void test_asc(int a[]) {
	gen_asc(a);
	clock_t start,end;
	start = clock();
	quickSort(a,0,N-1);
	end = clock();
	printf("升序数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}

// 测试3.降序数组
void test_desc(int a[]) {
	gen_desc(a);
	clock_t start,end;
	start = clock();
	quickSort(a,0,N-1);
	end = clock();
	printf("降序数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}


// 测试4.相同数组
void test_same(int a[]) {
	gen_same(a);
	clock_t start,end;
	start = clock();
	quickSort(a,0,N-1);
	end = clock();
	printf("相同数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}

当N取500000时,只有前两个测试有结果,降序数组和相同数组因为栈溢出而被迫终止。

当我们把N取40000时,几组结果才得以全部运行出来。

2.3总结

可以看出,我们这种算法对于已经有序的数组是非常不友好的。

所以我们更希望所给的数组最好是无序的,或者我们可以改进算法来弥补这些缺陷。

三、随机化取枢轴

前面我们说,对于那些有序的数组,时间复杂度会退化到O(N^2)。

那么我们就破坏这种有序的情况,每次递归选枢轴随机选择,这样大概率会破坏这些有序的数组,从而避免退化。

3.1划分原理

针对nums2和nums3的情况,假设每次选枢轴都是随机选取。

然后把随机选取的值和arrp交换,那么完全可以不用改动之前的逻辑。

于是得到如下代码:

cpp 复制代码
// 采用随机化取枢轴的划分方式
int partition2(int arr[], int p, int r) {
	int random = (rand() % (r-p + 1))+ p;
	int pivot = arr[random];
	arr[random] = arr[p];
	arr[p] = pivot;
	int minIndex = p + 1;
	int j = 0;
	for(j = p + 1; j <= r; j++) {
		if(arr[j] <= pivot) {
			int temp = arr[j];
			arr[j] = arr[minIndex];
			arr[minIndex] = temp;
			minIndex ++;
		}
	}
	arr[p] = arr[minIndex - 1];
	arr[minIndex - 1] = pivot;
	return minIndex - 1; 
}

3.2性能表现

N取40000就不会发生栈溢出了,而且对于有序数组来说,表现比随机数组还要优秀。

现在N取500000,测试一下

我们发现相同数组依旧栈溢出,但是有序数组表现非常优秀!!

3.3总结

可以发现,随机取枢轴能大幅度提高有序数组(不含大量相等元素的相等数组)的效率。

但是在面对相等数组时,还是克服不了栈溢出的问题,算法退化到O(N^2)

所以,我们想,能否结合随机化取枢轴,做一个"三路快排"。

把元素划分成三类,>pivot,=pivot,<pivot,对于=pivot的元素,我们只比较不做任何交换,那么是否会优化我们的算法呢?

四、三路快排

我们先看单纯的三路快排的情况。

还是取arrp为枢轴,现在我们需要维护三个区域,大区,小区,他们中间是等值区。

使用lt始终指向小区最右边界,gt始终指向大区最左边界,等值区不用管,维护lt,gt会自动恢复这个等值区。

4.1划分原理

初始情况,lt指向p,gt指向r,此时他两都是一个空区间。

从前向后遍历带划分区间,如果arri < pivot,则arri和arrlt交换,i++,lt++

如果arri > pivot则arri和arrgt交换,此时gt--但是i不变,因为arrgt是个大于pivot的数给交换到前面去了,需要二次检查。

如果arri == pivot则不交换,i++即可。

哪何时终止呢,其实只需要看i 是否超过gt即可,如果超过gt则直接停止,后面的数都是大于pivot的,所以要检查那些没有检查的数即i <= gt。

那么中间那些连续相同的元素,一定就排好序了,后面肯定不用动。所以在递归时就需要避开。

那么我们需要将lt和gt通过指针的形式带出来,交给quickSort()时,才知道那些是连续相同元素的区域。

这样便得到代码:

cpp 复制代码
// 三路快排
// 三路快排不需要返回值,只需要通过指针带回两个边界
void partition3(int arr[],int p, int r,int* lt,int* gt) {
	int pivot = arr[p];
	// 小区边界初始化成p
	*lt = p;
	// 大区边界初始化成r
	*gt = r;
	int j = 0;
	// 那么检查时从p开始检查
	for(j = p; j <= *gt; j++ ) {
		if(arr[j] < pivot) {
			int temp = arr[*lt];
			arr[*lt] = arr[j];
			arr[j] = temp;
			(*lt)++;
		}else if(arr[j] == pivot) {
			continue;
		}else if(arr[j] > pivot) {
			int temp = arr[*gt];
			arr[*gt] = arr[j];
			arr[j] = temp;
			(*gt)--;
			// 需要先j--回退一次,下一次要检查换回来的数
			j--;
		}
	}
	// 这样的话,就不需要再换arr[p]了,因为一开始检查了arr[p]它会回到指定位置的。
	// 最终arr[lt]...arr[gt] 都等于pivot
}

void quickSort3(int arr[],int p,int r) {
	if(p >= r) {
			return ;
		}
	int lt = 0,gt = 0;
	partition3(arr,p,r,&lt,&gt);
	quickSort3(arr,p,lt-1);
	quickSort3(arr,gt+1,r);
}

4.2性能表现

N取500000时,相同数组耗时非常快。

N取3000时

4.3总结

总结一下,三路排序针对大量相同元素的数组只需要花很少量的时间,性能非常高。

但是对于降序数组,容易栈溢出,性能没那么出众。

五、混合快排

5.1划分原理

现在我们将随机取枢轴和三路排块结合在一起。

有如下代码:

cpp 复制代码
// 混合快排
void partition4(int arr[], int p, int r, int* lt, int* gt) {
    // 1. 随机化取枢轴
    int random = p + rand() % (r - p + 1);
    int temp = arr[p];
    arr[p] = arr[random];
    arr[random] = temp;
    int pivot = arr[p];

    // 2. 初始化边界
    *lt = p;      // 小于区末尾
    *gt = r;      // 大于区开头
    int j = p + 1; // 跳过枢轴本身

    // 3. 核心遍历
    while (j <= *gt) {
        if (arr[j] < pivot) {
            (*lt)++; // 先扩展小于区
            int temp2 = arr[*lt];
            arr[*lt] = arr[j];
            arr[j] = temp2;
            j++;
        } else if (arr[j] > pivot) {
            int temp2 = arr[*gt];
            arr[*gt] = arr[j];
            arr[j] = temp2;
            (*gt)--; // j 不动
        } else {
            j++; // 等于 pivot
        }
    }

    // 4. 把枢轴换到小于区末尾(真正的交换!)
    int temp3 = arr[p];
    arr[p] = arr[*lt];
    arr[*lt] = temp3;
}

void quickSort4(int arr[],int p,int r) {
	if(p >= r) {
			return ;
		}
	int lt = 0,gt = 0;
	partition4(arr,p,r,&lt,&gt);
	quickSort4(arr,p,lt-1);
	quickSort4(arr,gt+1,r);
}

5.2性能表现

N取500000

性能非常炸裂。都很均衡。

5.3总结

我们发现混合快排的效率是非常高的,那么是不是混合快排在大量有序,大量随机的真实情况下仍然能打。

六、实战

现在容量为N的数组中,随机数组,升序,降序,等值数组数量均等。

现在整体测试一遍:

可以发现,混合三路排序力压群雄,性能最高。

七、代码

cpp 复制代码
#include <stdio.h>
#include <time.h>
#include <stdlib.h>

#define N 150000

int nums[N];


// 维护一个"小数字区的快排"
// 这种方法会把等于pivot的元素也会进行交换
// 例如 3 3 3 3 3 3 3
// 首先得到的分界点是6 比较了了7次左右,然后对3 3 3 3 3 3进行操作
// 又要比较了6次,下一次是5次,下一次4次,可见这样的复杂度最差,是O(N^2)
// 而且这种情况还会交换O(N^2)次,属于比较次数+交换次数最差情况
// 再例如 1 2 3 4 5 6 7
// 首先得到分界点6,比较6次,然后对1 2 3 4 5 6操作,比较5次,由此下去,大约O(n^2)次比较,O(N)次交换
// 再例如 7 6 5 4 3 2 1
// 首先得到分界点6,第一次比较了6次,结果为:1 6 5 4 3 2 
// 然后进入左右递归,右边递归为空区间,左边比较了5次
// 然后进入左递归,由此下去。。。
// 比较次数为O(N^2)次
// 交换次数为O(2N)
// 那对于更一般的情况,则是O(NlongN)
// 综上对于,有序数组(升序,降序,相同数组)都是最差情况
int partition(int arr[],int p, int r) {
	// 维护一个"小数字区间边界minIndex"保证minIndex前面的数都是小于等于pivot的
	int minIndex = p + 1;
	// 保存中枢
	int pivot = arr[p];
	int i = p+1;
	for(i = p+1; i <= r; i++) {
		if(arr[i] <= pivot) {
			// 放入小数字区后面 也就是说让arr[minIndex] 和 arr[i] 交换
			int temp = arr[minIndex];
			arr[minIndex] = arr[i];
			arr[i] = temp;
			// 小数字区向右延申
			minIndex ++;
		}
		// 考虑一件事情,是否会把和Pivot相等数字交换到右边去呢?
		// 答案是不会的,因为我们前面写了arr[i] <= pivot则保证了不会把和pivot相等的元素交换到后面去
	}
	// 此时小数字区末尾可能会越界,所以保险起见返回minIndex - 1
	// 最后交换pivot和minIndex - 1
	arr[p] = arr[minIndex - 1];
	arr[minIndex - 1] = pivot;
	return minIndex - 1;
}

// 采用随机化取枢轴的划分方式
int partition2(int arr[], int p, int r) {
	int random = (rand() % (r-p + 1))+ p;
	int pivot = arr[random];
	arr[random] = arr[p];
	arr[p] = pivot;
	int minIndex = p + 1;
	int j = 0;
	for(j = p + 1; j <= r; j++) {
		if(arr[j] <= pivot) {
			int temp = arr[j];
			arr[j] = arr[minIndex];
			arr[minIndex] = temp;
			minIndex ++;
		}
	}
	arr[p] = arr[minIndex - 1];
	arr[minIndex - 1] = pivot;
	return minIndex - 1; 
}

// 三路快排
// 三路快排不需要返回值,只需要通过指针带回两个边界
void partition3(int arr[],int p, int r,int* lt,int* gt) {
	int pivot = arr[p];
	// 小区边界初始化成p
	*lt = p;
	// 大区边界初始化成r
	*gt = r;
	int j = 0;
	// 那么检查时从p开始检查
	for(j = p; j <= *gt; j++ ) {
		if(arr[j] < pivot) {
			int temp = arr[*lt];
			arr[*lt] = arr[j];
			arr[j] = temp;
			(*lt)++;
		}else if(arr[j] == pivot) {
			continue;
		}else if(arr[j] > pivot) {
			int temp = arr[*gt];
			arr[*gt] = arr[j];
			arr[j] = temp;
			(*gt)--;
			// 需要先j--回退一次,下一次要检查换回来的数
			j--;
		}
	}
	// 这样的话,就不需要再换arr[p]了,因为一开始检查了arr[p]它会回到指定位置的。
	// 最终arr[lt]...arr[gt] 都等于pivot
}

// 混合快排
void partition4(int arr[], int p, int r, int* lt, int* gt) {
    // 1. 随机化取枢轴
    int random = p + rand() % (r - p + 1);
    int temp = arr[p];
    arr[p] = arr[random];
    arr[random] = temp;
    int pivot = arr[p];

    // 2. 初始化边界
    *lt = p;      // 小于区末尾
    *gt = r;      // 大于区开头
    int j = p + 1; // 跳过枢轴本身

    // 3. 核心遍历
    while (j <= *gt) {
        if (arr[j] < pivot) {
            (*lt)++; // 先扩展小于区
            int temp2 = arr[*lt];
            arr[*lt] = arr[j];
            arr[j] = temp2;
            j++;
        } else if (arr[j] > pivot) {
            int temp2 = arr[*gt];
            arr[*gt] = arr[j];
            arr[j] = temp2;
            (*gt)--; // j 不动
        } else {
            j++; // 等于 pivot
        }
    }

    // 4. 把枢轴换到小于区末尾(真正的交换!)
    int temp3 = arr[p];
    arr[p] = arr[*lt];
    arr[*lt] = temp3;
}

void quickSort4(int arr[],int p,int r) {
	if(p >= r) {
			return ;
		}
	int lt = 0,gt = 0;
	partition4(arr,p,r,&lt,&gt);
	quickSort4(arr,p,lt-1);
	quickSort4(arr,gt+1,r);
}

void quickSort3(int arr[],int p,int r) {
	if(p >= r) {
			return ;
		}
	int lt = 0,gt = 0;
	partition3(arr,p,r,&lt,&gt);
	quickSort3(arr,p,lt-1);
	quickSort3(arr,gt+1,r);
}

void quickSort2(int arr[],int p,int r) {
	if(p >= r) {
		return ;
	}
	int q = partition2(arr,p,r);
	quickSort2(arr,p,q-1);
	quickSort2(arr,q+1,r);
}

void quickSort(int arr[],int p, int r) {
	if(r <= p) {
		return ;
	}
	// 得到边界点
	int point = partition(arr,p,r);
	// 递归左区间
	quickSort(arr,p,point-1);
	// 递归右区间
	quickSort(arr,point+1,r);
}

// 生成N容量的随机数组
void gen_random(int a[]) {
	int i = 0;
	for(i = 0; i < N; i ++) {
		a[i] = rand() % 1000;
	}
}

// 生成N容量的升序数组
void gen_asc(int a[]) {
	for(int i=0;i<N;i++){
		a[i] = i;
	}
}

// 生成N容量的降序数组
void gen_desc(int a[]) {
	for(int i=0;i<N;i++){
		a[i] = N-1 - i;
	}
}

//生成N容量的一致数组
void gen_same(int a[]) {
	for(int i=0;i<N;i++){
		a[i] = 5;
	}
}

// 测试1.随机数组
void test_random(int a[]) {
	gen_random(a);
	clock_t start,end;
	start = clock();
//	quickSort(a,0,N-1);
//    quickSort2(a,0,N-1);
//	quickSort3(a,0,N-1);
	quickSort4(a,0,N-1);
	end = clock();
	printf("随机数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}

// 测试2.升序数组
void test_asc(int a[]) {
	gen_asc(a);
	clock_t start,end;
	start = clock();
//	quickSort(a,0,N-1);
//	quickSort2(a,0,N-1);
//	quickSort3(a,0,N-1);
	quickSort4(a,0,N-1);
	end = clock();
	printf("升序数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}

// 测试3.降序数组
void test_desc(int a[]) {
	gen_desc(a);
	clock_t start,end;
	start = clock();
//	quickSort(a,0,N-1);
//	quickSort2(a,0,N-1);
//	quickSort3(a,0,N-1);
	quickSort4(a,0,N-1);
	end = clock();
	printf("降序数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}


// 测试4.相同数组
void test_same(int a[]) {
	gen_same(a);
	clock_t start,end;
	start = clock();
//	quickSort(a,0,N-1);
//	quickSort2(a,0,N-1);
//	quickSort3(a,0,N-1);
	quickSort4(a,0,N-1);
	end = clock();
	printf("相同数组耗时:%.3f ms\n",(double)(end-start)/CLOCKS_PER_SEC*1000);
}




int main1() {
	// 测试
	test_random(nums);
	test_asc(nums);
	test_desc(nums);
	test_same(nums);
//	int arr[10] = {1,2,9,0,7,5,4,3,1,4};
//	quickSort4(arr,0,9);
//	int i = 0;
//	for(i = 0; i < 10; i++) {
//		printf("%d ",arr[i]);
//	}
//	printf("\n");
	return 0;
}

// ==========================================
// 大杂烩数据生成器:四种情况均等分配
// ==========================================
void gen_mixed(int a[]) {
    int quarter = N / 4; // 每个区块的大小
    
    // 1. 随机区块
    for(int i = 0; i < quarter; i++) {
        a[i] = rand() % 1000;
    }
    
    // 2. 升序区块
    for(int i = quarter; i < quarter * 2; i++) {
        a[i] = i - quarter;
    }
    
    // 3. 降序区块
    for(int i = quarter * 2; i < quarter * 3; i++) {
        a[i] = (quarter * 3) - i;
    }
    
    // 4. 相同区块 (全部填5)
    for(int i = quarter * 3; i < N; i++) {
        a[i] = 5;
    }
    
    // 可选:如果你想让这四块彻底打乱交织在一起,
    // 可以把下面这段取消注释,但这会让升序/降序的"恶意"变弱
    /*
    for(int i = N - 1; i > 0; i--) {
        int j = rand() % (i + 1);
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
    */
}

// ==========================================
// 统一的单次测试函数
// ==========================================
void test_all(int algo_type) {
    gen_mixed(nums); // 每次测试前,重新生成大杂烩数据
    
    clock_t start = clock();
    
    // 根据 algo_type 调用不同的算法
    if (algo_type == 1) quickSort(nums, 0, N - 1);
    else if (algo_type == 2) quickSort2(nums, 0, N - 1);
    else if (algo_type == 3) quickSort3(nums, 0, N - 1);
    else if (algo_type == 4) quickSort4(nums, 0, N - 1);
    
    clock_t end = clock();
    
    // 输出耗时
    double time_spent = (double)(end - start) / CLOCKS_PER_SEC * 1000;
    printf("%10.3f ms", time_spent);
}

// ==========================================
// 终极实战:四大快排大比拼
// ==========================================
void run_battle() {
    printf("===============================================================\n");
    printf("         大杂烩数据测试 (总容量 N = %d, 四种情况各占 25%%)\n", N);
    printf("===============================================================\n");
    printf("                      |  单指针  | 随机单指针 |  纯三路  |  混合三路 \n");
    printf("-----------------------------------------------------------------\n");
    
    printf("耗时:                 |");
    test_all(1); // 单指针
    test_all(2); // 随机单指针
    test_all(3); // 纯三路
    test_all(4); // 混合三路
    printf("\n");
    
    printf("===============================================================\n");
}

int main() {
    // 极其重要:初始化随机数种子
    srand((unsigned int)time(NULL)); 
    
    // 开启大比拼
    run_battle();
    
    return 0;
}
相关推荐
Coder_Shenshen44 分钟前
罗克韦尔ACD工程归档离线变量树解析
算法·c#
心仪久了会沦陷2 小时前
C语言数据结构入门——树与二叉树详解:概念、堆实现与链式遍历
c语言·数据结构·算法
小此方3 小时前
[算法实战论:动态规划]背包问题概念,01不满背包问题,01满背包问题,背包问题的滚动数组空间优化
算法·动态规划
悠仁さん4 小时前
【C++】哈希表
c++·算法·散列表
地平线开发者4 小时前
点云算法整理与常用计算模块:从数据到模型,再到工程与量化
算法·自动驾驶
蒸蒸yyyyzwd5 小时前
后端学习笔记 day111
笔记·学习
叼烟扛炮5 小时前
[LangChain] 学习笔记(08):工具调用底层闭环与第三方搜索(Tavily)实战
人工智能·笔记·学习·langchain
Century_Dragon6 小时前
让新能源故障诊断实训“触手可及“——仿真教学软件(VR)介绍
学习
武子康6 小时前
H3-World 为何在加载 LoRA 前,先检查导入的代码?
人工智能·深度学习·算法