九大排序算法(选择、冒泡、插入、快速、归并、堆、桶、计数、基数)的递归和非递归实现及并行思路(C语言)
void swap(float *a, float *b) {
float temp = *a; *a = *b; *b = temp; } // 交换两个浮点数
// 选择排序
// parallel: 并行查找最小值
void selection_sort(float *x, int size){
int min_idx; float tmp; for (int j = 0; j < size - 1; j++){
min_idx = j; for (int i = j + 1; i < size; i++){
if (x[i] < x[min_idx]) {
min_idx = i; } } swap(&x[j], &x[min_idx]); } }
// 冒泡排序
// parallel: 奇数阶段: 并行地比较和交换所有奇数索引与其右侧相邻元素的值;偶数阶段: 并行地比较和交换所有偶数索引与其右侧相邻元素的值。 重复执行奇数阶段和偶数阶段,直到数组有序。
void bb_sort(float *x, int size){
float tmp; bool flag = false;
for (int j = size -1; j >= 1; j-- ){
flag = false; for (int i = 0; i < j; i++){
if (x[i] > x[i + 1]){
swap(&x[i], &x[i+1]);flag = true;} } if (!flag) break; }
}
// 插入排序
// parallel: 分块插入排序,最后合并
void insertion_sort(float *x, int size){
float tmp; int i, j; for (i = 1; i < size; i++ ){
tmp = x[i]; j = i - 1; while (j >= 0 && x[j] > tmp){
x[j + 1] = x[j]; j--; } x[j + 1] = tmp; } }
// 快速排序
// parallel: 分区、并行、合并
int mid3(float *x, int low, int high) {
// 三数取中法,选取low, mid, high三个位置的元素,取其中值作为pivot
int mid = (low + high) / 2;
if (x[low] > x[mid]) swap(&x[low], &x[mid]); if (x[low] > x[high]) swap(&x[low], &x[high]); if (x[mid] > x[high]) swap(&x[mid], &x[high]);
return mid;
}
void partition(float *x, int size, int *pivot) {
// 根据pivot将数组分为两部分,左边的元素小于等于pivot,右边的元素大于pivot
int low = 0, high = size - 1, i = low - 1, j = low, pivotIndex = mid3(x, low, high);
float pivotValue = x[pivotIndex]; swap(&x[pivotIndex], &x[high]);
for (j = low; j < high; j++) {
if (x[j] <= pivotValue) {
i++; swap(&x[i], &x[j]); } }
swap(&x[i + 1], &x[high]); *pivot = i + 1;
}
void q_sort_rec(float *x, int size) {
if (size > 1) {
int pivot; partition(x, size, &pivot); q_sort_rec(x, pivot); q_sort_rec(x + pivot + 1, size - pivot - 1); } }
void q_sort_nrec(float *x, int size) {
int *stack, top_idx = -1, left, right, pivot;
// 最坏情况通常发生在每次分区时,枢轴总是选择到最小或最大的元素,递归深度会达到最大,即等于数组的大小 n,每次处理一个子数组时,会向栈中压入左右边界,即栈空间的需求为两倍。
stack = (int *)malloc(2 * size * sizeof(int)); stack[++top_idx] = 0; stack[++top_idx] = size - 1;
while (top_idx >= 0){
right = stack[top_idx--]; left = stack[top_idx--]; partition(x + left, right - left + 1, &pivot); pivot += left;
if (pivot - 1 > left) {
stack[++top_idx] = left; stack[++top_idx] = pivot - 1;} if (pivot + 1 < right) {
stack[++top_idx] = pivot + 1; stack[++top_idx] = right;}
}
free(stack);
}
// 归并排序
// parallel: 并行排序、并行合并规约
void merge(float *x, int sl, int sr){
float *l_start = x, *r_start = x + sl, *tmp = (float *)malloc((sl + sr) * sizeof(float)); int i = 0, j = 0, k = 0;
while (i < sl && j < sr){
if (l_start[i] <= r_start[j]) {
tmp[k++] = l_start[i++]; } else {
tmp[k++] = r_start[j++]; } }
while (i < sl) {
tmp[k++] = l_start[i++]; } while (j < sr) {
tmp[k++] = r_start[j++]; }
for (i = 0; i < sl + sr; i++) {
x[i] = tmp[i]; } free(tmp);
}
void merge_sort_rec(float *x, int size){
if (size > 1){
int sl = size / 2, sr = size - sl; merge_sort_rec(x, sl); merge_sort_rec(x + sl, sr); merge(x, sl, sr); } }
void merge_sort_nrec(float *x, int size) {
for (int width = 1; width < size; width *= 2) {
// width of each part : 1, 2, 4, 8, ...; e.g. 1 + 1 -> 2; 2 + 2 -> 4; 4 + 4 -> 8; ...
int left = 0;
while (left < size) {
// 合并每一对子数组
int mid = left + width - 1, right = (mid + width < size) ? mid + width : size - 1; // [left, mid], [mid+1, right] ; 此处限制了right的范围,避免超出数组的大小
if (mid < size - 1) {
merge(x + left, mid - left + 1, right - mid); }// 当前子数组确实有需要合并的右侧部分,才进行合并操作。
left += 2 * width;
}
}
}
// 堆排序
// parallel: 堆的构建:从最后一个非叶子节点开始,依次执行 heaptifyDown 操作。这些操作之间是相对独立的,可以并行执行。
void getLeftChild(int i, int *lc_idx) {
*lc_idx = 2 * i + 1; } // 获取左子节点的索引
void getRightChild(int i, int *rc_idx) {
*rc_idx = 2 * i + 2; } // 获取右子节点的索引
void heaptifyDown(float *arr, int size, int idx) {
// 比较当前节点与其左右子节点的大小。选择最小的子节点,如果当前节点大于这个子节点,则交换它们,并继续向下调整。
int cur = idx;
while (true) {
int lc, rc, minidx = cur; getLeftChild(cur, &lc); getRightChild(cur, &rc);
if (lc < size && arr[lc] < arr[minidx]) {
minidx = lc; } // 找到当前节点和左子节点中的最小值
if (rc < size && arr[rc] < arr[minidx]) {
minidx = rc; } // 找到当前节点和右子节点中的最小值
if (minidx == cur) break; // 如果当前节点是最小值,退出循环
float tmp = arr[cur]; arr[cur] = arr[minidx]; arr[minidx] = tmp; cur = minidx; // 交换当前节点和最小值节点, 更新 cur 为新的索引
}
}
void buildHeap(float *arr, int size){
// 使用heaptifyDown的原因是,如果使用 heaptifyUp从树的底部向上调整,每个节点在最坏情况下可能需要一直移到树的根部(并且底部的节点数量多)。
// 这意味着可能需要执行更多的比较和交换操作。而如果我们从上往下调整,每个节点最多只需要向下移动几层(通常是树的高度),这使得整体效率非常高。
for (int i = size / 2 - 1; i >= 0; i--) heaptifyDown(arr, size, i); // 从最后一个非叶子节点(size/2 - 1)开始,向上调整堆;
}
void heap_sort(float *heap, int size){
// 堆排序,从大道小排序
buildHeap( heap, size); // 构建小顶堆
while (size > 1) {
float tmp = heap[0]; heap[0] = heap[size - 1]; heap[size - 1] = tmp; size--; // 将堆顶元素与堆的最后一个元素交换,每次循环最小的元素被调整到末尾,堆的大小减1
heaptifyDown(heap, size, 0); // 从堆顶开始重新调整堆
}
}
// 桶排序
// parallel: 将元素分配到桶中的过程可以并行化;对每个桶中的元素进行排序也可以并行化;将桶中的元素取出也可以并行化。
int compare_floats(const void *a, const void *b) {
float fa = *(const float*)a, fb = *(const float*)b; return (fa > fb) - (fa < fb); }
void bucket_sort(float *arr, int size, int k){
if (k == 0) k = 4; // 默认桶的数量为4
float max_val, min_val, interval, **buckets = (float **)malloc(k * sizeof(float *)); int *bucket_size = (int *)malloc(k * sizeof(int)), idx;
if (size > 0) {
max_val = arr[0]; min_val = arr[0];} // 最大最小值用于计算桶间隔
for (int i = 0; i < k; i++) {
buckets[i] = (float *)malloc(sizeof(float) * size); bucket_size[i] = 0; }
for (int i = 1; i < size; i++) {
if (arr[i] > max_val) max_val = arr[i]; if (arr[i] < min_val) min_val = arr[i]; } interval = (max_val - min_val) / k;
if (max_val == min_val) {
for (int i = 0; i < k; i++) {
free(buckets[i]);} free(buckets); free(bucket_size); return;} // 计算桶的间隔;如果最大值和最小值相等,直接返回; 避免出现后续的除以0的情况
for (int i = 0; i < size; i++) {
idx = (int)((arr[i] - min_val) / interval); if (idx >= k) idx = k - 1; buckets[idx][bucket_size[idx]++] = arr[i]; } // 计算当前元素所属的桶;当 arr[i] 恰好等于 max_val 时,idx 会计算为 k,而 k 是桶的数量,这会导致数组越界。
for (int i = 0; i < k; i++) {
qsort(buckets[i], bucket_size[i], sizeof(float), compare_floats); } // 对每个桶进行排序
idx = 0; for (int i = 0; i < k; i++) {
for (int j = 0; j < bucket_size[i]; j++) {
arr[idx++] = buckets[i][j]; } free(buckets[i]); } free(buckets); free(bucket_size); // 将桶中的元素取出
}
// 计数排序
// parallel: 并行归约(reduction)技术来在多个线程中协同查找最大值; 并行计数; 并行前缀和计算; 并行拷贝
void counting_sort(int *arr, int size){
// 仅适用于非负整数
if (size <= 0) return; int max_val = arr[0]; for (int i = 1; i < size; i++) {
if (arr[i] > max_val) max_val = arr[i]; } // 找到最大值,用于确定计数数组大小[0, max_val]
int *counter = (int *) calloc(max_val + 1, sizeof(int)), *res = (int *)malloc(sizeof(int) * size); // 计数数组(最终存储前缀和);结果数组
for (int i = 0; i < size; i++) {
counter[arr[i]]++; } for (int i = 1; i <= max_val; ++i) {
counter[i] += counter[i - 1]; } // 计数和计算前缀和
for (int i = size - 1; i >= 0; i--) {
int cur_val = arr[i]; res[counter[cur_val] - 1] = cur_val; counter[cur_val]--; } // 从后往前遍历原数组,通过前缀和找到元素在结果数组中的位置,存入后,前缀和计数减1
memcpy(arr, res, sizeof(int) * size); free(counter); free(res);
}
// 基数排序
// parallel: 并行归约(reduction)技术来在多个线程中协同查找最大值; 并行计数; 并行前缀和计算; 并行拷贝; 多位并行处理
void get_kth_digit(int x, int k, int *digit){
while (k--) {
x /= 10; } *digit = x % 10; } // 获取数字x的第k位数字
void counting_sort_digit(int *arr, int size, int k){
int *counter = (int *) calloc(10, sizeof(int)), *res = (int *)malloc(sizeof(int) * size); // 计数数组(最终存储前缀和);结果数组
for (int i = 0; i < size; ++i) {
int digit; get_kth_digit(arr[i], k, &digit); counter[digit]++; } // 累计计数
for (int i = 1; i < 10; ++i) {
counter[i] += counter[i - 1]; } // 计算前缀和
for (int i = size - 1; i >= 0 ; --i) {
int digit; get_kth_digit(arr[i], k, &digit); res[counter[digit] - 1] = arr[i]; counter[digit]--; } // 从后往前遍历,根据前缀和确定元素在结果数组中的位置
memcpy(arr, res, sizeof(int) * size); free(counter); free(res);
}
void radix_sort(int *arr, int size){
// 仅适用于非负整数
if (size <= 0) return; int max_val = arr[0], max_digit = 0;
for (int i = 1; i < size; i++) {
if (arr[i] > max_val) max_val = arr[i]; } // 找到最大值,用于确定计数数组大小[0, max_val]
while (max_val > 0) {
max_val /= 10; max_digit++; } // 计算最大值的位数
for (int i = 0; i < max_digit; i++) {
counting_sort_digit(arr, size, i); } // 从低位到高位,依次对每一位进行计数排序
}