5.5.3 多参数值的函数

5.5.3 多参数值的函数

让我们回顾一下,写一个函数,能有哪些选项。在 F# 中,当写具有多个参数值的函数时,我们可以使用元组。我们下一个示例显示了一个函数,以这种风格加两个整数。我们将使用 lambda 函数的语法,但在 F# 中,使用简单的 let 绑定,也可以得到相同的结果:

> let add = fun (a, b) -> a + b;;
val add : int * int -> int

通过看类型签名,可以看到,该函数取一个参数值,这是一个元组的形式 (int * int),返回类型为 int。对应的 C# 的 lambda 函数,写成这样的形式:

Func<int, int, int> add =
(a, b) => a + b

Func < int、 int、 int > 委托表示一个方法,有两个 int 类型的参数值,并返回一个 int 类型,所以,这类似于使用元组写的 F# 版本。调用这个函数时,也可以看到这种相似性:

let n = add(39, 44)
var n = add(39, 44)

调用以元组作为参数值的 F# 函数(第一行),与调用 C# Func 委托(第二行)的语法是相同的。现在,让我们写相同的代码,在 F# 中,使用传统的 F# 样式,写有多个参数值的函数:

> let add = fun a b -> a + b;;
val add : int -> int -> int

令人惊讶的是,这与我们早先看到的在返回一个函数时的签名相同。可以读作, int-> (int-> int)。这就是一个函数,取第一个参数给这个加法,并返回一个函数。那么,结果是一个函数,取第二个参数值。我们可以重写这段代码,以这种方式,使用两个 lambda 函数,一个嵌套在另一个中:

> let add = fun a -> fun b -> a + b;;
val add : int -> int -> int

如果这是你第一次见到这种概念,它似乎很奇怪。一个函数返回另外一个函数,如何可以和函数,取另一个参数值并返回一个整数相同呢?有一个参数的函数如何可以和有两个参数的函数相同呢?不要太担心,如果不能马上理解——我们承诺,最终会理解的。在看过更多的示例之后,可能想转回到这一节,你就会更容易领会这个概念。

有多个元素的元组

如果要创建具有两个以上元素的元组,在 C# 中使用 .NET 4.0,可以使用重载的 Tuple 类,它提供重载表示的元组包含的元素从一个到八个。我们在第 3 章中实现的 C# 类有更多的限制,只支持仅 2 个元素。我们实现的重载,无论有多少个,总是会有一些限制。

不过,有一个方法可以克服这种限制。让我们看一下,我们如何可以使用来自第 3 章的 Tuple<A, B> 类型,表示 F# 的类型 int * string * bool。这个解决方案很简单,要存储比我们的元组类型支持的更多的元素,可以嵌套元组:

Tuple<int, Tuple<string, bool>> tup = (...);

当我们像这样声明一个变量时,它会携带三个值。要得到整型值,我们可以写成 tup.Item1;访问字符串值,写成 tup.tem2.Item1;最后,布尔值存储在 tup.Item2.Item2 中。

这是类似于嵌套函数,如 F# 类型 int -> (string -> bool)。在元组和函数之间有不同。此处所示的函数类型,与 int -> string –> bool 是同样的事情,而 F# 有三个元素的元组 (int * string * bool), 是不同于嵌套的元组类型,如 int * (string * bool)。

你可能想知道是否存在在 C# 3.0 中重写我们前面的示例的方法——确实可以。不要创建 Func<int, int, int> 类型的委托,可以创建一个 Func<int, Func<int, int>> 类型的委托,这更接近于 F# 函数的理解,签名为 int-> (int-> int):

Func<int, Func<int, int>> add =
a => b => a + b;
int n = add(39)(44);

写这个声明,用了两个 lambda 函数,就像我们前面的 F# 示例。当用此委托加数字时,我们必须调用第一个委托,它返回另一个委托;然后,调用第二个委托。在 F# 中,这是完全正常的、使用函数的方法,编译器会优化它,使之更有效率。

这是所有很有趣,但是,你可能会想,函数以这种方式的分离点是什么呢?原来是惊人的强大。

偏函数应用(PARTIAL FUNCTION APPLICATION)

要显示对函数的新理解是有用的情况,让我们把注意力返回到列表。假设我们有一个数字列表,我们想要为列表中的每个数字加上 10。在 F# 中,可使用 List.map 函数完成 ;在 C# 中,可以使用来自 LINQ 的 Select 方法:

list.Select(n => n + 10)
List.map (fun n -> n + 10) list

这已经相当简单了,但是还可以更简洁,如果我们已经有了来自前面示例中的 add 函数。List.map 期望作为第一个参数值的函数是 int -> int 类型。即,函数取一个整数作为参数值,并返回另一个整数。我们可以使用的技术称为部分偏函数应用(partial function application ):

> let add a b = a + b;;
val add : int -> int –> int

> let addTen = add 10;;
val addTen : (int -> int)

> List.map addTen [ 1 .. 10 ];;
val it : int list = [11; 12; 13; 14; 15; 16; 17; 18; 19; 20]

> List.map (add 10) [ 1 .. 10 ];;
val it : int list = [11; 12; 13; 14; 15; 16; 17; 18; 19; 20]

函数 add 的类型是 int -> int-> int。因为我们现在知道,它实际上意味着,该函数取一个整数并返回一个函数,因此,可以创建一个函数 addTen,把 10 加到给定的参数值上,通过调用 add,只有第一个参数值。然后,可以使用此函数作为参数值,给 List.map 函数。这有时很有用,但更重要的是,我们可以直接使用偏函数应用,在为 List.map 函数指定第一个参数值时。

函数 add 的类型是 int-> (int-> int),通过用一个数字作为参数值调用它,得到的结果类型是 int-> int,这正是 List.map 函数所需要的。当然,我们可以在 C# 中写相同的代码,如果使用嵌套的 lambda 函数声明 add 函数:

Func<int, Func<int, int>> add =
a => b => a + b;

list.Select(add(10));

正如我们在 F# 版本中看到的,调用 add 委托,得到结果类型 Func<int, int>,这与 Select 方法兼容。在 C# 中,使用具有多个参数值的 Func 委托,并使用另一个 lambda 函数为 Select 方法指定参数值,是更方便的,因为,语言支持更好。

注意

在使用偏函数应用时,会听到一个术语科里化(currying) 。它指的是,转换有多个参数值作(比如元组)的函数,到有第一个参数值的函数,并返回一个函数,有其余的参数,等等。例如,类型 int -> int-> int 的函数是(int * int)-> int 类型函数的科里化形式。那么,偏函数应用就是不指定所有参数值的科里化函数的使用。

正如我们已经提到,F# 中选择正确的样式可能很难。使用元组写的代码有时更容易读取大量的参数值,但它不能于偏函数应用。在这本书的其余部分,我们将使用感觉更适合各种情况的风格,这样,就可以获得的直观地理解哪个更好。最重要的是,我们使用元组,是在使代码更具可读性的情况下,和允许偏函数应用的风格,这种情况为我们提供了明显的好处。在下一章讨论高阶函数时,我们将会看到有关后者的很多例子。

<think>好的,我现在需要帮助用户解决如何用C++实现基于五元中值组取中值分割法的线性时间选择算法来找到第k小的元素。首先,我需要回忆一下这个算法的基本原理和步骤。 用户提到的“五元中值组取中值分割法”应该是指Median of Medians算法,也就是将数组分成每组5个元素,找到每组的中位数,然后从这些中位数中找到中位数的中位数作为基准点,这样可以保证较好的划分,从而在最坏情况下也能达到线性时间复杂度。这和快速选择算法类似,但通过这种分组方法避免了最坏情况的出现。 接下来,我需要确认算法的步骤。根据教材中的描述,大概分为以下几个步骤: 1. 将数组分成5元组,如果最后剩下的元素不足5个,单独成一组。 2. 对每个5元组进行排序,找出其中位数。 3. 递归调用算法,找到这些中位数的中位数作为基准(pivot)。 4. 使用这个基准将数组划分成小于、等于、大于基准的三部分。 5. 根据k的位置,决定递归处理左部分、右部分,或者直接返回基准值。 现在需要考虑如何在C++中实现这些步骤。可能需要编写几个辅助函数,比如找到中位数的函数,划分数组的函数,以及递归的主函数。 首先,对于将数组分成5元组的部分,可以使用循环来处理,每次取5个元素,然后排序取中位数。这里需要注意数组可能不是5的倍数,最后一组需要处理剩下的元素。 然后,递归调用算法来找中位数的中位数,这一步是关键,因为它确保了基准的选择足够好,从而保证划分后的数组不会太大或太小,进而保证线性时间。 划分数组时,可以使用类似快速排序中的分区方法,将数组分为小于、等于、大于基准的三部分。这样可以减少不必要的比较,特别是在有重复元素的情况下。 接下来,根据k的值判断应该在哪一部分继续递归查找。例如,如果k小于等于左部分的大小,则在左部分递归;如果在左部分和中间部分之间,则返回基准值;否则,在右部分递归,并调整k的值。 需要注意的是,处理数组时,可能需要使用起始和结束的索引来避免频繁复制数组,提高效率。C++中可以通过传递vector的引用和索引参数来实现这一点。 然后,测试代码的正确性。例如,给定一个数组和不同的k值,检查输出是否符合预期。同时,需要考虑边界条件,比如数组长度为1,或者k等于0的情况。 可能的错误点包括:递归终止条件是否正确,基准的选择是否正确,数组划分是否正确,以及索引处理是否正确。例如,在划分过程中,如何统计小于、等于、大于基准的元素数量,并正确计算它们的起始和结束位置。 此外,时间复杂度方面,每个递归调用处理的数组大小大约是原数组的7/10,因为中位数的中位数至少能保证有3/10的元素小于等于它,3/10大于等于它。这样总的时间复杂度为O(n)。 现在,编写代码的大致结构可能是这样的: 1.函数select_kth(),接受数组、左索引、右索引,和k。 2. 当数组大小小于等于5时,直接排序并返回第k个元素。 3. 否则,分割成5元组,找每个组的中位数,存入一个vector。 4. 递归调用select_kth()找到中位数中的中位数作为pivot。 5. 使用pivot划分数组,得到三个区间。 6. 根据k的位置决定递归处理哪部分。 在代码实现中,需要注意数组的索引处理,尤其是在递归调用时,确保传递正确的左右边界。另外,在划分过程中,如何高效地将元素分组,可以使用类似快速排序的交换方法,将小于pivot的移到左边,等于的中间,大于的右边。 还需要处理k的索引是否正确。比如,用户是否使用0-based还是1-based的k,这里可能需要明确。假设在问题中,k是1-based的话,在代码中可能需要调整。不过通常在编程中,使用0-based更常见,所以需要确认用户的需求。但根据问题描述中的“第k个最小的元素”,可能用户期望的是k从1开始。因此,在代码中可能需要将k调整为0-based或者在内部处理。 此外,关于参数传递,C++中的vector可以通过引用传递,避免不必要的拷贝,尤其是在处理大数组时效率更高。 现在,具体编写代码的时候,可能需要处理以下几个函数: - 一个辅助函数用于找到五个元素的中位数,可以通过排序后取中间位置的元素。 - 主函数实现递归选择,划分数组。 例如,辅助函数可以这样写: int medianOfFive(vector<int>& arr, int left, int right) { // 对arr[left..right]进行排序,取中位数 } 不过,因为每组是5个元素,所以当处理到最后一组可能不足5个时,需要处理剩下元素的中位数。例如,如果有n%5个元素,最后一组的大小为n%5,取其中位数。 另外,在划分数组时,可以遍历整个数组,将元素分为小于pivot、等于pivot、大于pivot的三部分,并统计它们的数量。然后根据k所在的位置决定下一步操作。 例如,如果k小于left部分的长度,则在左半部分递归查找;如果k在left和left+equal之间,返回pivot;否则在右半部分递归,并调整k的值。 最后,测试这个算法是否正确。比如,测试数组{12, 3, 5, 7, 4, 19, 26},找第3小的元素应该是5。或者更复杂的例子。 可能的错误包括:基准选择不正确,导致递归处理的部分不正确;或者在划分数组时,元素没有被正确归类到左中右三个区域;或者在递归调用时,传递的k值不正确。 此外,需要注意递归的终止条件是否正确,例如当数组大小小于等于5时直接排序返回结果,这时候要处理k的索引是否正确。 总结来说,实现这个算法需要仔细处理分组的步骤、递归调用以及数组划分,确保每个步骤都正确无误,才能保证最终的线性时间复杂度。</think>### C++实现Median-of-Medians线性时间选择算法 以下分步说明如何实现基于五元中值组取中值分割法的线性时间选择算法: #### 1. 算法原理 该算法通过两次中值选择保证最优划分,时间复杂度严格为$O(n)$[^1]。核心步骤包括: - 将数组划分为5元素一组 - 计算每组中值构成新数组 - 递归求新数组中值的中值作为基准 - 三路划分数组 #### 2. C++实现代码 ```cpp #include <vector> #include <algorithm> using namespace std; // 辅助函数:找五个元素的中位数 int medianOfFive(vector<int>& arr, int left, int right) { sort(arr.begin()+left, arr.begin()+right+1); return arr[left + (right-left)/2]; } // 主选择函数 int select_kth(vector<int>& arr, int left, int right, int k) { if (right - left + 1 <= 5) { sort(arr.begin()+left, arr.begin()+right+1); return arr[left + k - 1]; } // 分5元组求中值 vector<int> medians; for (int i = left; i <= right; i += 5) { int end = min(i + 4, right); medians.push_back(medianOfFive(arr, i, end)); } // 递归找中值的中值 int pivot = select_kth(medians, 0, medians.size()-1, medians.size()/2 + 1); // 三路划分 int i = left, l = left, r = right; while (i <= r) { if (arr[i] < pivot) { swap(arr[i++], arr[l++]); } else if (arr[i] > pivot) { swap(arr[i], arr[r--]); } else { i++; } } // 递归选择 int left_cnt = l - left; int mid_cnt = r - l + 1; if (k <= left_cnt) { return select_kth(arr, left, l-1, k); } else if (k <= left_cnt + mid_cnt) { return pivot; } else { return select_kth(arr, r+1, right, k - (left_cnt + mid_cnt)); } } // 接口函数 int findKthSmallest(vector<int>& arr, int k) { return select_kth(arr, 0, arr.size()-1, k); } ``` #### 3. 关键点说明 1. **分组处理**:`for`循环以5为步长划分子数组,处理剩余元素时使用`min(i+4, right)` 2. **基准选择**:递归调用`select_kth`获取中值的中值(第`medians.size()/2 + 1`小) 3. **三路划分**:将数组分为`< pivot`、`== pivot`、`> pivot`三个区间[^1] 4. **递归方向**:根据k值位置决定递归左/右区间或直接返回基准值 #### 4. 复杂度分析 - 时间复杂度:严格$O(n)$,递归深度为$O(\log n)$ - 空间复杂度:$O(\log n)$(递归栈空间) #### 5. 测试示例 ```cpp vector<int> test = {12, 3, 5, 7, 4, 19, 26}; cout << findKthSmallest(test, 3); // 输出5 cout << findKthSmallest(test, 6); // 输出19 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值