08 排序
- 1. 冒泡排序(BubbleSort)
- 1.1 循环嵌套实现
- 1.2 递归实现
- 2. 选择排序
- 2.1 嵌套循环实现
- 2.2 递归实现
- 3. 插入排序
- 4. 希尔排序
- 4.1 代码实现
- 5. 快速排序
- 5.1 代码实现
- 6. 归并排序
- 6.1 递归实现
- 6.2 循环实现
- 7. 堆排序
- 7.1 构建大顶堆
- 7.2 堆排序
- 7.3 代码实现
- 8. 计数排序
- 9. 桶排序
- 10. 基数排序
- 10.1 代码实现
1. 冒泡排序(BubbleSort)
冒泡排序是一种交换排序,基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序为止。
1.1 循环嵌套实现
// 循环嵌套实现
// len是元素个数
void BubbleSort1(int* arr, int len)
{for (int i = len - 1; i > 0; i--) // 比较len-1趟{// 每趟比较n-1个数据for (int j = 0; j < i; j++) // 只需要比较0,1,2,..,i之间的数据i之后的元素是已经排好序了{if (arr[j] > arr[j + 1]){int tmp = arr[j + 1];arr[j + 1] = arr[j];arr[j] = tmp;}}}
}
1.2 递归实现
每层递归中比较前n-1个元素,一共递归n趟
// 递归实现
void BubbleSort2(int* arr, int len)
{if (len < 2) // 元素为1个或0个不需要排序{return;}for (int i = 0; i < len - 1; i++)// 每趟比较0,1,...,n-1{if (arr[i] > arr[i + 1]){int tmp = arr[i + 1];arr[i + 1] = arr[i];arr[i] = tmp;}}BubbleSort2(arr, --len); // 下一趟
}
2. 选择排序
从头到尾扫描序列,找出最小的一个元素,和第一个元素做交换,接着从剩下的元素中继续这种选择和交换的方式,最终得到一个有序序列。
2.1 嵌套循环实现
// 循环嵌套实现选择排序
void SelectSort1(int* arr, int len)
{int i ;for (i = 0; i < len - 1; i++) // 一共需要len-1趟比较{int index =i; for (int j = i + 1; j < len; j++) // 每趟需要比较i+1,i+2,....,len-1之间的元素,i之前的元素是排序好的{if (arr[j] < arr[index]) //记录最小值的位置{index = j;}}// 找到最小值,如不过不是第一个元素,则交换第一个元素和最小值if (index != i){int tmp = arr[index];arr[index] = arr[i];arr[i] = tmp;}}
}
2.2 递归实现
// 递归实现选择排序
void SelectSort2(int* arr, int len)
{if (len < 2) // 1个或两个元素不需要排序{return;}// 每趟循环需要找出最小值位置int index = 0;for (int i = 1; i < len; i++){if (arr[i] < arr[index]) // 记录最小值位置{index = i;}}if (index != 0) // 交换首个元素和最小值{int tmp = arr[index];arr[index] = arr[0];arr[0] = tmp;}SelectSort2(arr + 1, --len);
}
3. 插入排序
插入排序原理:通过构建有序序列,对未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
// 插入排序
void InsertSort(int* arr, int len)
{int i,j = 0;for (i = 1; i < len; i++)// 对第一个数不需要排序,从第二个数开始,从后向前比较{int tmp = arr[i]; // 待排序元素,已经排序元素为arr[0]~arr[i-1]for (j = i - 1; j >= 0; j--) // 从已经排序好的元素的最右边开始,从后向前遍历{if (arr[j] <=tmp) // 找到小于当前元素则不需要再后移了,此时需要插入到小于当前元素之后的一位{break;}// 把大于当前待排序元素的元素后移arr[j + 1] = arr[j];}// 插入当前元素arr[j + 1] = tmp;}
}
4. 希尔排序
希尔排序(Shell)是插入排序的一种,它是针对直接插入排序算法的改进。希尔排序的基本思想:把待排序的数列分成多个组,然后对每个组进行插入排序,先让数列整体大致有序,然后多次调整分组方式,使数列更加有序,最后再使用一次插入排序,整个数列将全部有序。
流程:
- 初始
- 分组
- 对每个组进行插入排序
- 调整元素间隔,重新分组,并对每组进行插入排序
- 调整元素间隔,重新分组,并对每组进行插入排序
希尔排序的核心思想是化远为近:1)查找次数减少;2)移动元素次数减少
4.1 代码实现
希尔排序是在插入排序的基础上,每次取固定步长的间隔组成的分组,在其中进行插入排序。具体代码实现
// 间隔step执行插入排序
// start_pos每组的起始位置
// step 每组每个元素之间间隔的步长
void GroupSort(int* arr, int len, int start_pos, int step)
{int i=0, j=0;for (i = start_pos + step; i < len; i += step) // arr[start_pos]是第一个元素,跳过,从第二个开始即start_pos+step{int tmp = arr[i];//待排序袁旭,其中 arr[start_pos],...,arr[start_pos+step*(i-1)]是排序好的for (j = i - step; j >= 0; j -= step)// 从后往前组内进行插入排序{if (arr[j] <= tmp){break;}arr[j + step] = arr[j]; // 组内右移元素}// 插入当前元素arr[j + step] = tmp;}
}void ShellSort(int* arr, int len)
{// step是步长,每次都为原来的一半取整数,最后一次必定为1for (int step = len / 2; step > 0; step = step / 2){// 此时分为了step个组,对step个组内进行间隔step之间的元素插入排序for (int j = 0; j < step; j++){GroupSort(arr, len, j, step);}}
}
5. 快速排序
快速排序的基本思想是:
- 先从数列中取出一个元素作为基准数
- 扫描数列,将比基准数小的元素放在它的左边,大于或等于基准数的元素全部放到它的右边,得到左右两个区间
- 再对左右区间重复第1,2步,直到各个区间小于两个元素
流程:挖坑填数+分治思想
-
初始化
-
这里以左指针指向的第一个为基准数,以右指针指向的第一个数开始扫描数组:1<4,将它插入到4左指针指向的位置;移动左指针
-
3<4,继续移动左指针;5>4,将5填入到右指针指向的位置;移动右指针,7>4,继续移动右指针,指向2
-
2<4 将2填入到左指针指向位置,移动左指针,左指针指向6,6>4,将6填入到右指针指向的位置,移动右指针
-
此时左指针和右指针重叠,把基准元素插入到左右指针指向的位置。
-
下一次比较从当前基准数的左区间和右区间分别开始迭代。
可以看到快速排序算法需要维护左指针,右指针,维护基准数。
5.1 代码实现
具体的代码实现如下:
void QuickSort(int* arr, int len)
{if (len < 2){return;}int tmp = arr[0]; // 选取最左边的数作为基准数int left = 0; // 左下标int right = len - 1; // 右下标int moving_flag = 2; // 移动左下标还是右下标标志while (left < right){if (moving_flag == 2) // 移动右下标{if (arr[right] >= tmp) // 大于基准数,则继续移动右下标{right--;continue;} // 小于基准数,把它填入到左下标对应的坑中arr[left] = arr[right];// 左下标移动left++;// 下次将继续移动左下标moving_flag = 1;continue;}if (moving_flag == 1) // 移动左下标{if (arr[left] < tmp) // 小于基准数,则继续移动左下标{left++;continue;}// 大于基准数,把它填入到右下标对应的坑中arr[right] = arr[left];// 移动右下标right--;moving_flag = 2;// 下次移动右下标continue;}}// 循环结束,左右下标重合,填入基准数的值arr[left] = tmp;// 对基准数左边区间进行排序QuickSort(arr, left);// 对基准数右边区间进行排序QuickSort(arr + left + 1, len - left - 1);
}
6. 归并排序
归并排序(Merge Sort)就是将已经有序的子数列合并得到另一个有序的数列。
归是归并的归,不是递归,归并排序就是合并排序。
流程:
-
初始化
-
归并排序
-
继续归并
-
最终结果
6.1 递归实现
流程:
代码
// 利用递归实现归并排序
// arr是待排序数组
// arr_tmp是用于排序的临时数组的首地址
// start是排序区间第一个元素的位置
// end是排序区间最后一个元素的位置
void _mergeSort(int* arr,int* arr_tmp, int start,int end)
{if (start >= end) // 表示该区间的元素少于两个,递归终止{return;}int mid = (start + end) / 2; // 计算排序区中间的位置int start1 = start, end1 = mid; // 左区间int start2 = mid + 1, end2 = end;// 右区间_mergeSort(arr, arr_tmp, start1, end1); // 对左边区间进行递归拆分_mergeSort(arr, arr_tmp, start2, end2); // 对右边区间进行递归拆分// 对区间内的元素进行合并排序// 把区间左右两边合并到已经排序数组arr_tmp中// 从arr_tmp的start开始int i = start;while (start1 <= end1 && start2 <= end2){arr_tmp[i++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];}while (start1 <= end1) // 把左边剩下的追加到arr_tmp;{arr_tmp[i++] = arr[start1++];}while (start2 <= end2) // 把右边剩下的追加到arr_tmp{arr_tmp[i++] = arr[start2++];}// 把已经排序好的数组,复制到arr中memcpy(arr+start, arr_tmp+start, sizeof(int) * (end - start + 1));
}void MergeSort(int* arr, int len)
{// 创建临时数组int* arr_tmp = new int[len];memset(arr_tmp, 0, sizeof(int) * len);_mergeSort(arr, arr_tmp, 0, len - 1);delete[]arr_tmp;
}
6.2 循环实现
// 利用循环实现归并排序
void mergeSort(int* arr, int len)
{// 创建临时数组,用于存放已排序元素int* arr_tmp = new int[len];memset(arr_tmp, 0, sizeof(int) * len);for (int i = 1; i < len; i = i * 2) // 每趟选取i个元素作为左右区间{for (int j = 0; j < len; j = j + i * 2) // 把len长度按照i*2 拆分{int start = j; // 起始int mid = min(start + i, len); // 中间 ,考虑分配不均int end = min(start + i * 2, len); // 结尾,考虑分配不均的情况int k = start;int start1 = start, end1 = mid;int start2 = mid, end2 = end;while (start1 < end1 && start2 < end2){arr_tmp[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];}// 把左边剩余元素追加while (start1 < end1){arr_tmp[k++] = arr[start1++];}// 把右边剩余元素追加while (start2 < end2){arr_tmp[k++] = arr[start2++];}}int* ptr_tmp = arr;arr = arr_tmp;arr_tmp = ptr_tmp;}if (arr!=arr_tmp){memcpy(arr, arr_tmp, sizeof(int) * len);}delete [] arr_tmp;
}
7. 堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆具备以下的特点:
- 完全二叉树(从上到下,从左到右都是满的,最后一层的结点都连续集中在最左边),对于用数组存储的完全二叉树有:N[i]的左子结点N[2i+1],右子结点N[2i+2],父结点N[(i-1)/2]
- 二叉树每个结点的值都大于等于其左右子树结点的值称为大顶堆;或者每个结点的值都小于等于其左右子树结点的值称为小顶堆。
7.1 构建大顶堆
流程:
首先利用heapify即元素下沉的方法,从最后一个元素的父结点开始,不断下沉小元素,构建一个大顶堆
- 比较结点的两个子结点,如果结点值都大于两个子结点,则不交换。不断向前
- 比较两个子结点,如果结点值小于子结点,取子结点最大值的那个结点与结点交换。并对交换后的结点进行同样的操作。
如图中所示,结点0的左子结点和右子结点都大于0,取最大的那个结点即6,与0交换。此时0没有左子结点和右子结点则完成。
- 比较两个子结点,如果结点值小于子结点,取子结点最大值的那个结点与结点交换。并对交换后的结点进行同样的操作。
如图中所示,结点1小于结点8,交换两个结点。之后继续比较结点的左子结点和右子节点,此时两个子结点都大于1,取最大的子结点7,交换。
此时已经完成了所有结点的比较,构建出了一个大顶堆。可以看到8>6,8>7,6>5,6>0,7>1,7>4,5>2,5>3。
用循环实现heapify操作
void Swap(int* elem1, int* elem2)
{int tmp = *elem2;*elem2 = *elem1;*elem1 = tmp;
}
// 元素下沉
// 待排序数组地址,start-待下沉元素结点的下标,end待排序数组最后一个元素的下标
void heapify(int* arr, int start, int end)
{// 确定下沉结点和左子结点位置int parent = start;int son = 2 * start + 1; // 先比较左子结点while (son <= end)// 下标不越界{// 比较两个子结点的大小if ((son+1 <= end) && (arr[son] < arr[son+1])) // 右子结点存在 且 左子结点小于右子结点{son++; // 两个子节点中大的是右子节点}if (arr[parent] > arr[son]) // 比左右子结点都大,则不需要交换元素{return;}Swap(&arr[parent], &arr[son]); // 交换父子结点内容parent = son; // 继续向下son = 2 * parent + 1; // 左子结点}
}void HeapSort(int* arr, int len)
{// 从最后一个元素的父结点开始初始化堆for (int i = (len - 1) / 2; i >= 0; i--){heapify(arr, i, len - 1);}
}
用递归实现heapify操作
// 递归实现heapify
void heapify2(int* arr, int start, int end)
{// 确定下沉结点和左子结点位置int parent = start;int son = 2 * start + 1;if (son > end){return;}if ((son + 1 <= end) && arr[son] < arr[son + 1]) // 选择左右子结点最大的{son = son + 1;}if (arr[parent] > arr[son]) // 待下沉结点比左右子结点都大,无需下沉{return;}// 交换父结点与大的子结点Swap(&arr[parent], &arr[son]);// 对子结点进行heapifyheapify2(arr, son, end);
}
7.2 堆排序
在将数组初始化为大顶堆之后,交换首个元素与待排序数组的最后一个元素的位置(此时带排序数组的最后一个元素一定是最大的元素),并从交换后的首个元素进行下沉操作,除掉交换后的最后一个元素,构建大顶堆,如此循环。
7.3 代码实现
void Swap(int* elem1, int* elem2)
{int tmp = *elem2;*elem2 = *elem1;*elem1 = tmp;
}// 元素下沉
// 待排序数组地址,start-待下沉元素结点的下标,end待排序数组最后一个元素的下标
void heapify(int* arr, int start, int end)
{// 确定下沉结点和左子结点位置int parent = start;int son = 2 * start + 1;while (son <= end)// 下标不越界{// 比较两个子结点的大小if ((son+1 <= end) && (arr[son] < arr[son+1])) // 右子结点存在 且 左子结点小于右子结点{son++; // 两个子节点中大的是右子节点}if (arr[parent] > arr[son]) // 比左右子结点都大,调整完毕{return;}Swap(&arr[parent], &arr[son]); // 否则交换父子结点内容parent = son; // 继续向下son = 2 * parent + 1;}
}// 堆排序
void HeapSort(int* arr, int len)
{// 从最后一个元素的父结点开始初始化堆for (int i = (len - 1) / 2; i >= 0; i--){heapify(arr, i, len - 1);}for (int i = 0; i < len; i++){cout << arr[i] << " ";}cout << endl;// 把待排序数组第一个元素和最后一个元素交换,并从第一个元素开始进行下沉操作for (int i = len - 1; i > 0; i--){Swap(&arr[0], &arr[i]);heapify(arr, 0, i - 1);}
}
8. 计数排序
计数排序原理很简单,申请一个计数数组,遍历待排序数组,对每个出现的数据元素在计数数组的对应下标进行计数。遍历完成后,根据计数数组计数个数,对数据进行还原。
代码实现:
// 获取数组中最大元素的值
int array_max(int* arr, int len)
{int max_elem = arr[0];for (int i = 1; i < len; i++){if (arr[i] > max_elem)max_elem = arr[i];}return max_elem;
}void CountSort(int* arr, int len)
{int max_elem = array_max(arr, len);int* arr_tmp = new int[max_elem + 1];memset(arr_tmp, 0, sizeof(int) * (max_elem + 1));// 计数for (int i = 0; i < len; i++){arr_tmp[arr[i]]++;// arr_tmp[arr[i]]的计数值++}// 重新填充int ii = 0;for (int j = 0; j < max_elem + 1; j++) // 遍历arr_tmp,下标对应的就是原数组的元素{for (int k = 0; k < arr_tmp[j]; k++) // 填充arr_tmp[jj]个{arr[ii++] = j;}}
}
9. 桶排序
桶排序(BucketSort)的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,然后再对每个桶分别排序(可以使用冒泡排序,快速排序等排序方法),最后把全部桶的数据合并。
void BubbleSort(int* arr, int len)
{for (int i = len - 1; i > 0; i--) // 比较len-1趟{// 每趟比较n-1个数据for (int j = 0; j < i; j++) // 只需要比较0,1,2,..,i之间的数据i之后的元素是已经排好序了{if (arr[j] > arr[j + 1]){int tmp = arr[j + 1];arr[j + 1] = arr[j];arr[j] = tmp;}}}
}void BucketSort(int* arr, int len)
{int bucket[5][5]; // 分配5个桶,实际按照数据大小调整,每个桶最多五个元素int bucketsize[5]; // 每个桶中的元素个数计数memset(bucket, 0, sizeof(bucket));memset(bucketsize, 0, sizeof(bucketsize));// 把数据arr放入桶中for (int i = 0; i < len; i++){bucket[arr[i] / 10][bucketsize[arr[i] / 10]++] = arr[i];}// 对每个桶中的数据进行快速排序for (int i = 0; i < 5; i++){BubbleSort(bucket[i], bucketsize[i]);}// 再把每个桶中的数据填充到arr中int k = 0;for (int i = 0; i < 5; i++){for (int j = 0; j < bucketsize[i]; j++){arr[k++] = bucket[i][j];}}
}
10. 基数排序
基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
具体做法是:将所有带比较数字统一为一样的数位长度,数位较短的数前面补零。然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,就变成了一个有序数列。
- 第一次分配和收集
- 第二次分配和收集
- 第三次分配和收集
10.1 代码实现
// 获取最大元素
int getMax(int* arr, int len)
{int max_elem = arr[0];for (int i = 1; i < len; i++){if (arr[i] > max_elem){max_elem = arr[i];}}return max_elem;
}// 按base位进行分配
void _radixsort(int* arr, int len, int base)
{int* arr_tmp = new int[len];memset(arr_tmp, 0, sizeof(int) * len);int bucket[10] = { 0 }; // 初始化桶0,1,2,...,9for (int i = 0; i < len; i++) // 按位数计数{bucket[arr[i] / base % 10] ++;}// 累加bucketfor (int i = 1; i < 10; i++){bucket[i] = bucket[i] + bucket[i - 1];}// 存放for (int i = len - 1; i >= 0; i--){arr_tmp[bucket[arr[i] / base % 10] - 1] = arr[i];bucket[arr[i] / base % 10]--;}memcpy(arr, arr_tmp, sizeof(int) * len);delete[] arr_tmp;
}void RadixSort(int* arr, int len)
{// 获取最大数值,决定分配收集的次数int max_elem = getMax(arr, len);int base = 1; // 从个位开始while (max_elem / base != 0){_radixsort(arr, len, base);base = base * 10; // 向前一位}
}