文章目录
- 冒泡排序
- 选择排序
- 插入排序
- 归并排序
- 快速排序(重点讲解)
- 堆排序(重点理解)
- [408考研各数据结构C/C++代码(Continually updating)](#408考研各数据结构C/C++代码(Continually updating))
冒泡排序
时间复杂度 O(n2)
空间复杂度 O(1)
冒泡排序的思想是,从第一个开始遍历,然后每次都把比较大的数据
移动到后面去,那么在第一次交换的时候,最大的数据已经到最后去了,
以此类推,第二次冒泡的话,倒数第二个就是倒数第二大的数据。
c
#include <stdio.h>
void bubbleSort(int arr[], int n)
{
if (arr == NULL || n < 2)
{
return;
}
int flag = 0;
// 这是第几轮
for (int i = 0; i < n; i++)
{
flag = 0;
// 这一层的for循环的意思就是当前轮需要包含的数据的个数
// 因为每一次冒泡,都会有一个最大的数据到末尾去
// 所以需要不断的减少需要参与计算的数据量
for (int j = 0; j < n - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
// 使用异或交换
arr[j] = arr[j] ^ arr[j + 1];
arr[j + 1] = arr[j] ^ arr[j + 1];
arr[j] = arr[j] ^ arr[j + 1];
flag = 1;
}
}
//如果此时已经没有数据更新,那么说明已经有序,直接跳出循环
if (flag == 0)
{
break;
}
}
}
int main()
{
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
bubbleSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
选择排序
时间复杂度 O(n^2)
空间复杂度 O(1)
思路是从第一个开始遍历,然后每次都选择最小的哪一个放到前面去。
分区间: 将数组分为两部分,一部分是已排序的,另一部分是未排序的。初始时,整个数组都被视为未排序。
找最小值: 在未排序的部分中,查找最小的元素。通常,这是通过遍历未排序部分的所有元素并不断更新一个临时最小值和其索引来实现。
交换位置: 一旦找到最小元素,将其与未排序部分 的第一个元素交换位置,使其成为已排序部分的一部分。
增加已排序部分的大小: 将已排序部分的大小递增,同时减少未排序部分的大小。
重复: 重复步骤2到步骤4,直到整个数组都被排序。未排序部分会不断减小,而已排序部分会逐渐增加。
完成: 当未排序部分为空时,排序过程完成,整个数组被排序。
上面是我早期写的笔记,比较书面。口语的意思其实就是,在开始排序之前,先记录下一次要插入的最小的数据的位置,一开始其实就是0,然后记录下来这个起始位置p,并且设定这个起始位置的值为暂定的min最小值,然后从起始位置开始遍历整个数组,找到最小的那个数据,并且记录下来这个位置的索引,并且更新我们暂定的min的值为这个遍历到的更小的值。
之后,我们遍历完毕一次数组之后,我们就得到了从起始位置到达数组末尾中的那个最小值的索引和最小值。
此时,我们进行交换,将这个最小值交换到我们一开始设定的其实位置p,当然,我们肯定不能直接就覆盖了这个起始位置的值,还需要将我们一开始设定的起始位置的值覆盖掉这个我们后来找到的最小值的索引,不然这个最小值就会一直参与计算了。
c
#include <stdio.h>
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int min = arr[i]; // 初始化最小值为当前元素
int p = i; // 用于记录最小值的索引
// 在未排序部分查找最小值
for (int j = i + 1; j < n; j++) {
if (min > arr[j]) {
min = arr[j];
p = j;
}
}
// 如果找到更小的值,交换它们
if (p != i) {
int temp = arr[p];
arr[p] = arr[i];
arr[i] = temp;
}
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
selectionSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
插入排序
时间复杂度 O(n2)
空间复杂度 O(1)
插入排序的概念是:每次选取一个索引,从0开始。然后判断当前数据是不是比前面一个小,
如果是,那么就开始交换,并且一直交换,直到比前面的大,或者前面没有数据了为止。
因此插入在高度有序的情况下,时间复杂度O(n),最差O(n2)
插入排序一般性能都比冒泡和选择排序好 因为他会在数据有序的时候直接结束内循环。
这里比较重点的就是,插入排序每次遍历的时候,是向前遍历,也就是每次都要确保当前元素是大于前面元素的,如果不成立,也就是arr[j]>arr[j+1],那么插入排序就会开始向前交换元素。直到不成立或者遇到索引0.
我甚至认为插入排序最简单,因为他实际有效的代码就两行!!!
c
#include <stdio.h>
void insertSort(int arr[], int n) {
//从第一个元素开始遍历,然后内循环比较当前元素的前面一个元素和当前元素的关系
for (int i = 1; i < n; i++) {
// 这里的条件判断就是 j 前面还有数据,并且前面的 j 比后面的数据大
// 那么就需要进行一次交换
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
// 使用异或交换
arr[j] = arr[j] ^ arr[j + 1];
arr[j + 1] = arr[j] ^ arr[j + 1];
arr[j] = arr[j] ^ arr[j + 1];
}
}
}
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
insertSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
归并排序
数组版本
时间复杂度 O(nlogn)
空间复杂度 O(n)
归并排序的思路:
使用递归的方式不断将数组进行拆分,拆分为左右两个等大小的数组。
由于递归的特性,最后会拆分为长度最小为2的一个小区间,之后我们就对这个小区间进行排序即可。
最后,我们就可以得到多个的有序的小区间。
然后递归返回之后,又会继续对这些小区间合并后的一个区间进行合并,然后不断递归返回,
就可以得到最终有序的一个区间了。
而递归调用的一个重要思路就是,使用一个临时数组,这个临时数组用于存放左右区间里的数据,并且会按照大小进行排序。
思路为,使用p1(左指针),使用p2(右指针)的方式,左指针指向left,右指针指向mid+1,
然后左右指针不断后移,直到越界,左指针边界为left---mid,右指针边界为mid+1---right。
然后将左右指针中指向的更小的数据拷贝到临时数组中,此时临时数组中的元素在区间内有序。
之后将这个区间内有序的数据,覆盖原有的数组即可。
之所以归并排序可以把时间复杂度缩短到nlogn,是因为相对于时间复杂度n2的排序,这些排序浪费了大量的比较功能
而归并排序每次都会比较两个范围的数据,并且将其合并成一个区间内有序的数据,然后再将这个区间去和更大的区间进行
一次排序,那么这样子,他的比较的操作就没有浪费,而是保留了下来。
代码如下:
c
#include <stdio.h>
// 提前声明merge函数
void merge(int arr[], int left, int middle, int right);
void mergeSort(int arr[], int left, int right);
void mergeSort(int arr[], int left, int right)
{
if (left == right)
{
return;
}
//划分中轴
int mid = ((right - left) >> 1) + left;
//对左边进行归并
mergeSort(arr, left, mid);
//对右边进行归并
mergeSort(arr, mid + 1, right);
//排序
merge(arr, left, mid, right);
}
//对left到right这个区间上的数据进行排序
void merge(int arr[], int left, int mid, int right)
{
// 创建一个辅助空间 left--right上有多少个数就开多大的空间
int help[right - left + 1];
int i = 0; // 提供给help使用的变量
int p1 = left; // 左侧数据的下标
int p2 = mid + 1; // 指向mid+1位置
// 判断p1和p2是否越界,如果都不越界
// 那么谁小谁就拷贝到数组help中去
while (p1 <= mid && p2 <= right)
{
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 继续判断是否有没有拷贝完毕的数据
// 可能是左半部分的p1没有拷贝完毕
while (p1 <= mid)
{
help[i++] = arr[p1++];
}
// 也可能是右侧的p2没有拷贝完毕
while (p2 <= right)
{
help[i++] = arr[p2++];
}
// 将help上面有序的数据拷贝到原数组上去,就得到了区间上有序数据
for (i = 0; i < (sizeof(help) / sizeof(help[0])); i++)
{
arr[left + i] = help[i];
}
}
int main()
{
int arr[] = {12, 11, 13,2312,123,556,887, 5, 6, 7};
int arr_size = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
for (int i = 0; i < arr_size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
mergeSort(arr, 0, arr_size - 1);
printf("排序后的数组:\n");
for (int i = 0; i < arr_size; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
链表版本
相对于使用数组,使用链表的归并排序的空间复杂度为O(1)。
时间复杂度不变。
链表方式的归并排序与数组方式的归并排序有一些不同,因为链表不支持随机访问,需要特别考虑如何拆分和合并链表。
大概的实现思路如下:
分割链表: 首先,将原始链表拆分为两个子链表。可以使用快慢指针方法,即使用两个指针,一个慢指针每次前进1步,另一个快指针每次前进2步,找到链表的中点。然后,将链表从中点分为两个子链表。重复这个过程,直到链表不能再分割。
递归排序: 对分割得到的两个子链表递归应用归并排序。这将重复分割和排序的过程,直到每个子链表只包含一个元素或为空。这些子链表是已排序的。
合并有序链表: 合并两个有序链表,这是归并排序的核心操作。创建一个新的链表作为合并结果,然后从两个子链表中逐个选择较小的节点,并将其添加到新链表中,直到两个子链表都为空。这将创建一个有序的合并链表。
返回结果: 返回合并后的有序链表,这就是排序完成的链表。
c
#include <stdio.h>
#include <stdlib.h>
struct ListNode {
int val;
struct ListNode* next;
};
struct ListNode* merge(struct ListNode* l1, struct ListNode* l2) {
if (!l1) return l2;
if (!l2) return l1;
struct ListNode dummy;
struct ListNode* tail = &dummy;
dummy.next = NULL;
while (l1 && l2) {
if (l1->val < l2->val) {
tail->next = l1;
l1 = l1->next;
} else {
tail->next = l2;
l2 = l2->next;
}
tail = tail->next;
}
if (l1) tail->next = l1;
if (l2) tail->next = l2;
return dummy.next;
}
struct ListNode* findMiddle(struct ListNode* head) {
if (!head) return NULL;
struct ListNode* slow = head;
struct ListNode* fast = head;
while (fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
struct ListNode* mergeSort(struct ListNode* head) {
if (!head || !head->next) return head;
struct ListNode* middle = findMiddle(head);
struct ListNode* secondHalf = middle->next;
middle->next = NULL;
struct ListNode* left = mergeSort(head);
struct ListNode* right = mergeSort(secondHalf);
return merge(left, right);
}
void printList(struct ListNode* head) {
struct ListNode* current = head;
while (current != NULL) {
printf("%d ", current->val);
current = current->next;
}
printf("\n");
}
void insert(struct ListNode** head, int val) {
struct ListNode* newNode = (struct ListNode*)malloc(sizeof(struct ListNode));
newNode->val = val;
newNode->next = *head;
*head = newNode;
}
int main() {
struct ListNode* head = NULL;
int n, val;
printf("输入链表元素的数量: ");
scanf("%d", &n);
printf("请开始输入链表元素:\n");
for (int i = 0; i < n; i++) {
scanf("%d", &val);
insert(&head, val);
}
printf("Original list:\n");
printList(head);
struct ListNode* sortedList = mergeSort(head);
printf("Sorted list:\n");
printList(sortedList);
return 0;
}
快速排序(重点讲解)
时间复杂度O(nlogn)
空间复杂度O(n)
快速排序是我们最常用的排序算法,也是性能比较好的一种时间算法。
因此我会重点讲解快速排序的实现思路。
在上面我们提到了归并排序,其实现的思想是将对整个数组的操作,通过递归的方式,变为对一小块区间的排序操作,也就是说,合理的使用递归等过程,是可以优化性能的。那么快排也可以。
可以思考如下一个场景,假设有一个数组,元素内容是:1,3,5,3,4,6,7,1,2,6。
假设我们有一个需求,是通过给定一个数据,然后要求我们的代码使得数组中的小于等于这个数据的元素出现在这个元素的左边,而如果是大于这个数据的元素,出现在数组的右边。
也就是相当于我们通过给定的这个数,对数组进行了一个划分,对于给出的数target,恒有
数组左侧于target的数据,小于target,数组右侧于target的数据,大于target。
那么很容易通过双指针的方式得到这样子的代码:
c
//指定一个数据 然后比这个数据大的数据放在右边,比这个数据小于等于的放在左边。
void smallLeftBigRight(int arr[],int target){
int slow = 0;
int fast = 0;
while (fast<arr.length){
if (arr[fast]<=target){
arr[slow]^=arr[fast];
arr[fast]^=arr[slow];
arr[slow]^=arr[fast];
slow++;
}
fast++;
}
}
输入一个数组之后,我们就可以发现,数组中某个位置左右的数据是小于和大于我们给出的数据的,如下:
可以发现,经过我们简单的这样子的操作,数据好像看起来有了那么一点顺序。
是的,快排就是基于这样子的一个思想:设定一个基准,要求基准值左边的数据小于等于基准值,基准值右边的数据大于基准值。并且通过递归的方式,直到所有的区间符合要求。
那么如果我们要实现上面的说法,应该如何实现呢:
实现思路1:
依旧是上面的数组,我们选定最后的一个数据为基准(设定为target),然后要求小于等于这个数据的出现在这个数据的左边,大于这个数据的出现在这个数据的右边。
然后我们通过遍历的方式,从左到右遍历这个数组,如果遇到的值小于target,就继续后移,因为合理,如果遇到的值大于target,我们就将这个值交换到数组的末尾去,这样子大的值就放到后面了,当然,如果是这种情况,由于我们不确定换过来的数据是否大于target,我们的指针是不能增加的,然后在进行一次上面的比较才确定是否增加。循环往复,直到右边交换的值都已经比当前target大。
那么此时我们就按照上面的代码中得到了这样子一个部分有序的数据了,然后此时我们将大于这个数据的第一个数据与这个最后一个数据进行交换(交换后target的索引为index),那么我们此时就得到了,这个target数据左边的数据都小于这个数据,右边的都大于这个数据。
然后我们继续缩短这个范围,根据这个数据划分出来的左右边界,继续执行这个流程,左边的那一边从最左边的索引开始left,然后右侧范围为index, 而右边也重复这样子的过程,其左边界为index+1,右边界为right。不断执行这样子的递归操作,直到左指针和右指针碰撞,我们就可以返回。
按照这样子的思路,我们可以发现,其时间复杂度最差为O(n2),因为在数据高度有序的情况下,例如:1,2,3,4,5。那么我们的过程其实每次都是这个数据自己和自己交换,因为左边的都是比它小的。大概的一个流程如下:
所以,按照我们上面的意思,快排的时间复杂度在这个基准值非常差劲的时候,时间复杂度
O(n2)。压根就不快,所以我们需要解决基准值很偏的问题。
代码实现如下,我这里设定最左边的数据为中轴。
c
/**
* 下面是对代码的逐步解释:
* <p>
* public static int[] quickSortX(int[] arr, int start, int end):这是快速排序的入口函数,
* 它接受一个整数数组 arr 以及排序范围的起始位置 start
* 和结束位置 end 作为参数,并返回排序后的整数数组。
* <p>
* int pivot = arr[start];:选择数组中的第一个元素作为中轴值(pivot)。
* <p>
* int left = start; 和 int right = end;:定义两个指针,left 从左向右移动,right 从右向左移动,
* 用于在数组中找到需要交换的元素。
* <p>
* 下面的 while (left < right) 循环是快速排序的核心部分,它在数组中找到需要交换的元素,
* 以确保中轴值左边的元素都小于等于中轴值,中轴值右边的元素都大于等于中轴值。这个循环包含以下几个部分:
* <p>
* 第一个 while 循环:从左边开始,找到一个大于或等于中轴值的元素。
* 第二个 while 循环:从右边开始,找到一个小于或等于中轴值的元素。
* 如果找到的左边元素等于右边元素,则继续将 left 向右移动,以避免出现无限循环。
* 否则,交换左边元素和右边元素的值,确保左边元素小于中轴值,右边元素大于中轴值。
* 之后,代码检查是否有需要递归排序的左半部分和右半部分。如果左半部分的起始位置小于左指针的前一个位置,
* 递归调用 quickSortX 对左半部分进行排序。同样,如果右半部分的结束位置大于右指针的后一个位置,递归调用
* quickSortX 对右半部分进行排序。
* <p>
* 最后,函数返回排序后的数组 arr。
*
* @param arr
* @param start
* @param end
* @return
*/
int[] quickSortX(int arr[], int start, int end) {
int pivot = arr[start];
int left = start;
int right = end;
//范围合法性
while (left < right) {
//设定pivot中轴值左边的数据要求比pivot小
while (left < right && arr[left] < pivot) {
left++;
}
//设定pivot中轴值右边的数据要比pivot大
while (left < right && arr[right] > pivot) {
right--;
}
//循环停止时 存在 arr[right]<=pivot>=arr[left]
//如果等于 那么继续让left指针后移一位
if (left < right && arr[left] == arr[right]) {
left++;
} else {//否则就是直接交换他们两个的值即可
//此时存在 arr[left]<pivot<arr[right]
//交换数据
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}
if (start < left - 1) {
arr = quickSortX(arr, start, left - 1);
}
if (right + 1 < end) {
arr = quickSortX(arr, right + 1, end);
}
return arr;
}
这个时候,按照我们上面的思想,我们肯定希望我们的中轴值没有那么偏僻,所以我们考虑使用一个随机的方式,生成我们中轴的位置。那么我们按照master方式,可以得到最快的效率O(nlogn)。
所以我们就有了目前使用的快速排序,思路如下:
实现思路2:
c
/**
* void quickSort(int[] arr):这是公共的快速排序入口函数。
* 它接受一个整数数组 arr 并检查是否需要排序。如果数组为空或只包含一个元素,它就不执行排序。否则,它调用
* quickSortO2 来进行快速排序。
* <p>
* void quickSortO2(int[] arr, int left, int right):这个函数执行实际的快速排序算法。
* 它接受数组 arr 以及排序的范围从 left 到
* right。该函数使用了随机选择中轴值的方法,然后调用 partition 函数来划分数组,并递归地对左右两部分进行排序。
* <p>
* swap 函数用于交换数组中的两个元素。这个函数通过位运算进行交换,是一种非常快速的方法。
* <p>
* int[] partition(int[] arr, int left, int right):这个函数用于处理 arr[left...right]
* 上的数据,将数组按照中轴值进行划分。它返回一个包含两个元素的整数数组,
* 表示等于中轴值的区域的左边界和右边界。这个函数采用双指针法,其中 lessBound
* 表示小于区的右边界,moreBound 表示大于区的左边界。
* <p>
* 首先,通过将 arr[right] 作为中轴值,初始化 lessBound 和 moreBound。
* 使用 left 指针从左到右遍历数组元素。
* 如果当前元素小于中轴值,将其与 lessBound 右边的元素交换,并将 lessBound 和 left 向右移动。
* 如果当前元素大于中轴值,将其与 moreBound 左边的元素交换,并将 moreBound 向左移动。
* 如果当前元素等于中轴值,只将 left 指针向右移动。
* 最后,将 arr[right] 与 moreBound 处的元素交换,将数组划分为小于、等于和大于中轴值的三个部分。
* 返回 lessBound + 1 和 moreBound,它们分别表示等于区的左边界和右边界。
* <p>
* 这里,对于partition方法,我的实现和思考思路如下:
* <p>
* 初始化 lessBound 为 left - 1,即 lessBound 初始为-1,表示小于区的右边界。
* <p>
* 初始化 moreBound 为 right,即 moreBound 初始为10,表示大于区的左边界。
* <p>
* 使用 left 指针从左到右遍历数组元素。
* <p>
* 当 arr[left](当前元素)小于 arr[right](中轴值)时,执行以下操作:
* 交换 arr[left] 和 arr[lessBound + 1],然后增加 lessBound 和 left。
* 这表示我们将小于区的右边界向右扩展一个位置,并将当前元素放入小于区。
* 当 arr[left] 大于 arr[right] 时,执行以下操作:
* 交换 arr[left] 和 arr[moreBound - 1],然后减少 moreBound。
* 这表示我们将大于区的左边界向左扩展一个位置,并将当前元素放入大于区。
* 当 arr[left] 等于 arr[right] 时,只将 left 指针向右移动,因为相等的元素将留在等于区。
* 最终,left 指针遍历整个数组,将数组划分为三个部分:小于区、等于区和大于区。
* <p>
* 为了完成划分,将 arr[right](中轴值)与 arr[moreBound](大于区的左边界)进行交换。这将把中轴值放到正确的位置。
* <p>
* 返回一个包含两个元素的数组,[lessBound + 1, moreBound]。这个数组表示等于区的左边界和右边界。
* <p>
* 其中,对于 arr[left] > arr[right] 这种情况,我并没有left++,是因为:
* 当前元素 arr[left] 大于中轴值 arr[right] 时,我们将它放入大于区,即 arr[moreBound - 1] 处。
* 这意味着 moreBound 表示大于区的左边界。
* <p>
* 通过减少 moreBound 的值,我们将大于区的左边界向左移动,同时保持 left 指针不变。
* 这是因为当前元素 arr[left] 已经被放入大于区,而在下一次迭代中,我们需要继续检查新的 arr[left]
* 是否大于中轴值,以确保大于区包含所有大于中轴值的元素。
* <p>
* 所以,left 指针只在当前元素小于中轴值时执行 ++ 操作,表示将当前元素放入小于区,而在当前元素大于中轴值时,
* 只需要更新大于区的左边界 moreBound,而不需要改变 left 指针。
*
* @param arr
*/
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
quickSortO2(arr, 0, arr.length - 1);
}
public static void quickSortO2(int[] arr, int left, int right) {
if (left < right) {
swap(arr, left + (int) (Math.random() * (right - left + 1)), right);
int[] p = partition(arr, left, right);
quickSortO2(arr, left, p[0] - 1); //< 区域
quickSortO2(arr, p[1] + 1, right); //> 区域
}
}
//当前方法用于处理arr[left...right]上面的数据
//默认以arr[right]做划分
//返回等于区域(左边界、右边界),所以返回一个长度为2的数组res,res[0],res[1]
public static int[] partition(int[] arr, int left, int right) {
int lessBound = left - 1;//小于区右边界
int moreBound = right; //大于区左边界
while (left < moreBound) {//left表示当前数据的位置 arr[right] -->划分值
if (arr[left] < arr[right]) { //当前数据小于划分值
swap(arr, ++lessBound, left++);
} else if (arr[left] > arr[right]) {
swap(arr, --moreBound, left);
} else {
left++;
}
}
swap(arr, moreBound, right);
return new int[]{lessBound + 1, moreBound};
}
public static void swap(int[] arr, int i, int j) {
//特别注意 如果i==j,那么会导致数据被抹除
if (i == j) {
return;
}
arr[i] ^= arr[j];
arr[j] ^= arr[i];
arr[i] ^= arr[j];
}
对于记忆的话,还是选择可读性比较好的方法1吧,追求性能的时候可以考虑使用方法2。
堆排序(重点理解)
堆排序的时间复杂度是稳定的O(nlogn)
空间复杂度为O(1)
堆排序的思想是基于完全二叉树的。
堆排序会使得数据满足大顶堆或者小顶堆的特性。大顶堆的意思就是非叶子节点的值大于叶子节点。小顶堆则相反。
因此对于一个堆,其插入数据不符合堆的特性的时候,是需要进行堆化的,也就是会将这个数据进行排序,移动到合理的位置,使其满足大顶堆的特性。
堆排序的详细步骤如下:
建立堆:从数组的中间元素开始,从右至左,或者从下至上,对每个元素执行下沉操作(即将元素下沉到适当的位置),以确保树的每个分支满足堆的性质。这将创建一个有效的最大堆或最小堆,取决于排序需求。
排序:堆建立完成后,根节点包含堆中的最大(或最小)元素。将根节点与堆的最后一个元素交换,然后缩小堆的范围,即排除掉最大(或最小)元素。接着,对根节点执行一次下沉操作,以确保新的根节点仍然是堆中的最大(或最小)元素。重复这个步骤,直到堆为空。
重复:重复步骤2,直到整个数组被排序。
大部分讲解堆的博客中都会明确告诉你,堆排序是需要在数组的末尾不断缩短边界的,把堆顶元素和数组末尾元素进行交换,然后在进行一次堆化,不断循环往复,就可以得到一个有序的数组。
这就是堆排序的精髓。
当然当然,堆其实最最最最重要的不是堆排序,而是堆的结构,合理利用堆的结构可以解答非常非常非常多的题目。
比如如下这题:
考研408真题
c
#include <stdio.h>
// 升序排序堆排序 --- 使用大顶堆
void adjustHeap(int arr[], int i, int length) {
int temp = arr[i]; // 将当前非叶子结点保存
for (int k = 2 * i + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) { // 判断左孩子大还是右孩子大
k++; // 如果右孩子大就++获得右孩子的索引
}
if (arr[k] > temp) { // 判断孩子大还是当前节点大
arr[i] = arr[k]; // 将更大的向上排
i = k; // 大的元素就跑到了非叶子结点的索引去,然后让 i 去更小数据的索引
} else {
break;
}
}
arr[i] = temp; // 大的数据被移上去了,但是其原有位置的数据还没有被修改
}
// 降序排序堆排序 --- 使用小顶堆
void adjustHeapDesc(int arr[], int i, int length) {
int temp = arr[i]; // 将当前非叶子结点保存
for (int k = 2 * i + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] > arr[k + 1]) { // 判断左孩子大还是右孩子大
k++; // 如果右孩子小就++获得右孩子的索引
}
if (arr[k] < temp) { // 判断孩子小还是当前节点小
arr[i] = arr[k]; // 将更小的向上排
i = k; // 小的元素跑到了非叶子结点的索引,然后让 i 去更大数据的索引
} else {
break;
}
}
arr[i] = temp; // 小的数据被移上去了,但是其原有位置的数据还没有被修改
}
void heapSort(int arr[], int length) {
// 构建小顶堆
for (int i = length / 2 - 1; i >= 0; i--) {
adjustHeapDesc(arr, i, length);
}
printf("初始构造小顶堆: ");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// 将堆顶元素与末尾元素进行交换,将最小数据沉到末尾
// 重新调整结构,使其继续满足小顶堆性质
for (int j = length - 1; j > 0; j--) {
int temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
// 重新调整堆,排除已排序部分,使其继续满足小顶堆性质
adjustHeapDesc(arr, 0, j);
}
printf("小顶堆排序后的数组(降序排列): ");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int arr[] = {4, 10, 3, 5, 1};
int length = sizeof(arr) / sizeof(arr[0]);
printf("原始数组: ");
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
heapSort(arr, length);
return 0;
}
408考研各数据结构C/C++代码(Continually updating)
408考研各数据结构C/C++代码(Continually updating)
这个模块是我应一些朋友的需求,希望我能开一个专栏,专门提供考研408中各种常用的数据结构的代码,并且希望我附上比较完整的注释以及提供用户输入功能,ok,fine,这个专栏会一直更新,直到我认为没有新的数据结构可以讲解了。
目前我比较熟悉的数据结构如下:
数组、链表、队列、栈、树、B/B+树、红黑树、Hash、图。
所以我会先有空更新出如下几个数据结构的代码,欢迎关注。 当然,在我前两年的博客中,对于链表、哈夫曼树等常用数据结构,我都提供了比较完整的详细的实现以及思路讲解,有兴趣可以去考古。