数据结构:排序详解(使用语言:C语言)

ops/2025/3/13 16:02:01/

1.排序的概念及其运用

1.1排序的概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。

内部排序:数据元素全部放在内存中的排序。

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2排序运用

 

1.3 常见的排序算法 

2、常见排序算法思想

2.1 插入排序

       2.11 基本思想:

        把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的   记录插入完为止,得到一个新的有序序列 。当我们排序扑克牌是就是用的这种思想。

2.12 直接插入排序

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。

代码演示:

void InsertSort(int* a, int n)
{for (int i = 0;i < n - 1;i++){int tmp = a[i + 1];int end = i;while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = tmp;}
}

注:插入排序一般都是在原数组上进行操作,不用再创建一个数组 。可以结合代码和上图一同看。

直接插入排序的特性总结:

1. 元素集合越接近有序,直接插入排序算法的时间效率越高

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1),它是一种稳定的排序算法

4. 稳定性:稳定

2.1.3 希尔排序( 缩小增量排序 )

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。

简单来说希尔排序就是把一个数组变得比较有序,然后再进行插入排序。因为通过简单的计算我们能发现,插入排序在有序的情况下时间复杂度是O(N)。

代码演示:

void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){gap /= 2;for (int i = 0;i < n - gap;i++){int tmp = a[i + gap];int end = i;while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end = end - gap;}else{break;}}a[end + gap] = tmp;}}
}

 注:如果是一趟一趟看的话建议可以多加一层循环,时间复杂度还是不变的。这里gap就相当于前面插入排序的1,只不过这里跳的步数大。

1. 希尔排序是对直接插入排序的优化。

2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就 会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的 希尔排序的时间复杂度都不固定。

4. 稳定性:不稳定

2.2  选择排序

2.2.1基本思想:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的 数据元素排完 。

2.2.2 直接选择排序:

在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

代码演示:

void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void SelectSort(int* a, int n)
{int left = 0;int right = n - 1;int max = 0;int min = 0;while (left < right){max = left;min = right;for (int i = left;i <= right;i++){if (a[i] > a[max]){max = i;}if (a[i] < a[min]){min = i;}}swap(&a[left], &a[min]);if (left == max){max = min;}swap(&a[right], &a[max]);left++;right--;}
}

注:代码中的swap函数为交换函数 ,当左指针指向的值为max时,我们需要加一个把max的值重新换回来的判断,否则会将最大值与最小值交换,那么最大值的指针指向的就不是最大值了。反之同理。

直接选择排序的特性总结:

1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:不稳定 

2.2.3 堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

代码演示:


void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void adjustdwon(int* a, int n, int parent)
{int child = (parent * 2) + 1;while (child < n){if (child + 1 < n && a[child] < a[child + 1]){child++;}if (a[child] > a[parent]){swap(&a[child], &a[parent]);parent = child;child = (parent * 2) + 1;}else{break;}}
}
void HeapSort(int* a, int n)
{int i = 0;for (i = (n - 1 - 1) / 2;i >= 0;i--){adjustdwon(a, n, i);}int end = n - 1;while (end > 0){swap(&a[end], &a[0]);adjustdwon(a, end, 0);end--;}
}

注: 因为向下调整建大堆时,要左右子树都为大堆因此我们要从最后一个不是叶子节点的 位置开始向下调整建大堆,而这个位置就是最后一个叶子节点的父亲。每次拿到最大值以后与最后位置交换,之后就不要动最大值了。

直接选择排序的特性总结:

1. 堆排序使用堆来选数,效率就高了很多。

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(1)

4. 稳定性:不稳定

 2.3 交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2.3.1冒泡排序

基本思想:两个数字相互比较大的换到小的前面,如此反复。当一趟走完以后,最大的会在最后面。

代码演示:

void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
void BubbleSort(int* a, int n)
{for (int j = 0;j < n;j++){for (int i = 1;i < n - j;i++){if (a[i - 1] > a[i]){swap(&a[i - 1], &a[i]);}}}}

 冒泡排序的特性总结:

1. 冒泡排序是一种非常容易理解的排序

2. 时间复杂度:O(N^2)

3. 空间复杂度:O(1)

4. 稳定性:稳定

 2.3.2 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

1. hoare版本

可以先看一个动图:

代码演示:

void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] < a[right]){if (a[midi] > a[left]){return midi;}else if (a[midi] < a[left]){return left;}else{return right;}}//(a[left]>a[right])else{if (a[midi] >a[right]){return midi;}else if (a[midi] < a[right]){return right;}else{return left;}}
}
int PartSort1(int* a, int left, int right)
{//随机取值/*int randi = left + (rand() % (right - left));swap(&a[left], &a[randi]);int keyi = left;*///三数取中int midi = getmidi(a, left, right);swap(&a[left], &a[midi]);int keyi = left;while (left < right){while (left < right && a[right]>=a[keyi]){right--;}while (left < right && a[left] <= a[keyi]){left++;}swap(&a[left], &a[right]);}swap(&a[left], &a[keyi]);keyi = left;return keyi;
}
void QuickSort(int* a, int left, int right)
{if (left >= right){return;}int keyi = PartSort1(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

 注:代码里面的随机取值是为了让key值取一个比较随机的值,因为当数组为有序时,如果用常规的Hoare快排会让时间复杂度变成(N^2)下面将用一个图解释一下。

这里一般是有两种方法第一个就是取随机值像上面代码呈现的一样,第二个就是三数取中在左边中间右边位置,取一个中间值。如果遇到大量重复数据,快排的效率会大大降低,这里就需要三路划分的方法。( keyi代表key值的下标)

2. 挖坑法

挖坑其实和 hoare版本十分相似,唯一不同的就是将key值拿到外面去制造一个坑位(这里注意还是要保持左边做key右边先走的思想,这里是左边的值拿出来右边就先走)然后右边找到一个小于key值的数放到坑里。左边再找小于key的值放到右边坑位,如此反复。

代码演示:
 


void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] < a[right]){if (a[midi] > a[left]){return midi;}else if (a[midi] < a[left]){return left;}else{return right;}}//(a[left]>a[right])else{if (a[midi] >a[right]){return midi;}else if (a[midi] < a[right]){return right;}else{return left;}}
}
int PartSort2(int* a, int left, int right)
{int midi = getmidi(a, left, right);if (midi != left)swap(&a[left], &a[midi]);int key = a[left];int hole = left;while (left < right){while (left < right && a[right] >= key)right--;a[hole] = a[right];hole = right;while (left < right && a[left] <= key)left++;a[hole] = a[left];hole = left;swap(&a[left], &a[hole]);}a[hole] = key;return hole;
}
void QuickSort(int* a, int left, int right)
{if (left >= right){return;}int keyi = PartSort2(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

3. 前后指针版本


void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] < a[right]){if (a[midi] > a[left]){return midi;}else if (a[midi] < a[left]){return left;}else{return right;}}//(a[left]>a[right])else{if (a[midi] >a[right]){return midi;}else if (a[midi] < a[right]){return right;}else{return left;}}
}
int PartSort3(int* a, int left, int right)
{int midi = getmidi(a, left, right);if (midi != left)swap(&a[left], &a[midi]);int prev = left;int cur = left + 1;int keyi = left;while (cur <= right){if (a[cur] < a[keyi]){prev++;swap(&a[cur], &a[prev]);cur++;}else{cur++;}}swap(&a[keyi], &a[prev]);keyi = prev;return keyi;
}
void QuickSort(int* a, int left, int right)
{if (left >= right){return;}int keyi = PartSort3(a, left, right);QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}

 2.3.3 快速排序非递归

 因为快排与二叉树的前序遍历十分相像,因此快排的非递归我们能用栈来实现。

具体步骤如下图:

void swap(int* p1, int* p2)
{int tmp = *p1;*p1 = *p2;*p2 = tmp;
}
int getmidi(int* a, int left, int right)
{int midi = (left + right) / 2;if (a[left] < a[right]){if (a[midi] > a[left]){return midi;}else if (a[midi] < a[left]){return left;}else{return right;}}//(a[left]>a[right])else{if (a[midi] >a[right]){return midi;}else if (a[midi] < a[right]){return right;}else{return left;}}
}
void QuickSortNonR(int* a, int left, int right)
{st st;stinit(&st);stpush(&st, right);stpush(&st, left);while (!stempty(&st)){int begin = sttop(&st);stpop(&st);int end = sttop(&st);stpop(&st);int keyi= PartSort3(a, begin,end);if (keyi + 1 < end){stpush(&st, end);stpush(&st, keyi + 1);}if ( keyi - 1>begin){stpush(&st, keyi-1);stpush(&st, left);}}
}

快速排序的特性总结:

1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序

2. 时间复杂度:O(N*logN) 

3. 空间复杂度:O(logN)

4. 稳定性:不稳定

2.4 归并排序

基本思想:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

归并排序核心步骤:

 

void _MergeSort(int*a, int* tmp, int begin,int end )
{if (begin >= end){return;}int mid = (end + begin) / 2;_MergeSort(a, tmp, begin,mid);_MergeSort(a, tmp, mid+1, end);int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int j = begin;while (begin1<=end1 && begin2<=end2){if (a[begin1] < a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}memcpy(a + begin, tmp + begin, sizeof(int)*(end - begin + 1));}
void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}_MergeSort(a, tmp, 0, n - 1);free(tmp);
}

2.4.1 归并排序的非递归

void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}int gap = 1;while (gap < n){for (int i = 0;i < n;i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;if (end1 >= n || begin2 >= n){break;}if (end2 >= n){end2 = n - 1;}int j = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap = 2 * gap;}}

注:begin1 end1 begin2 end 2那部分代码不理解的可以自己套数 

 归并排序的特性总结:

1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2. 时间复杂度:O(N*logN)

3. 空间复杂度:O(N)

4. 稳定性:稳定

2.5 非比较排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。

操作步骤: 1. 统计相同元素出现次数

2. 根据统计的结果将序列回收到原来的序列中

void CountSort(int* a, int n)
{int max = a[0];int min = a[0];for (int i = 1;i < n;i++){if (max < a[i]){max = a[i];}if (min > a[i]){min = a[i];}}int range = max - min+1;int* counta = (int*)malloc(sizeof(int) * range);if (counta == NULL){perror("malloc fail");return;}memset(counta, 0, sizeof(int) * range);//计数for (int i = 0;i < n;i++){counta[a[i] - min]++;}//排序int j = 0;for (int i = 0;i< range;i++){while (counta[i]--){a[j++] = i+ min;}}free(counta);
}

注:这里计数数组映射的相对值,因此最后取出来要加最小值。

计数排序的特性总结:

1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。

2. 时间复杂度:O(MAX(N,范围))

3. 空间复杂度:O(范围) 比特科技

4. 稳定性:稳定

3.排序算法复杂度及稳定性分析


http://www.ppmy.cn/ops/165445.html

相关文章

数据分析人员需要掌握sql到什么程度?

学习SQL三个层次 熟悉基本的增删改查语句及函数&#xff0c;包括select、where、group by、having、order by、delete、insert、join、update等&#xff0c;可以做日常的取数或简单的分析&#xff08;该水平已经超过90%非IT同事&#xff09;;掌握并熟练使用高阶语法&#xff0…

蓝桥杯备考:新二叉树

这道题和正常的前序遍历没啥区别&#xff0c;就是改成char而已吧 #include <iostream> using namespace std; const int N 300; char l[N],r[N]; void dfs(char root) {if(root *) return;cout << root;dfs(l[root]);dfs(r[root]);} int main() {int n;cin >&…

Java链接redis

一、准备工作就像谈恋爱 首先咱们得来点仪式感是不是&#xff1f;打开你的Maven&#xff08;Gradle玩家别打我&#xff09;&#xff0c;把这两个宝贝依赖给我焊死在pom.xml里&#xff1a; <!-- 经典永不过时的Jedis --> <dependency> <groupId>redis.cli…

pdf修改内容:分享5款好用的工具

pdf修改内容用什么软件&#xff1f;PDF内容修改工具的便捷性很大地提升了我们的工作效率。通过这些工具&#xff0c;我们可以轻松地对PDF文档进行文字编辑、图片替换、页面调整等操作&#xff0c;无需繁琐的转换步骤。这些修改工具不仅操作简便&#xff0c;而且功能强大&#x…

通义万相 2.1 + 蓝耘算力,AI 视频生成的梦幻组合

在这个科技日新月异的时代&#xff0c;人工智能不断刷新着我们对世界的认知。一次偶然的机会&#xff0c;我借助北京蓝耘科技股份有限公司提供的算力支持&#xff0c;踏上了使用通义万相 2.1 进行 AI 视频生成的奇妙之旅。 目录 1.1初遇蓝耘科技&#xff1a; 1.2通义万相 2.1…

C++ 布尔类型(bool)深度解析

引言 在 C 编程里&#xff0c;布尔类型&#xff08;bool&#xff09;是一种基础且极为关键的数据类型。它专门用于表达逻辑值&#xff0c;在程序的条件判断、循环控制等诸多方面都发挥着重要作用。接下来&#xff0c;我们将对 C 中的布尔类型展开全面且深入的探讨。 一、布尔…

通用人工智能(AGI):定义、挑战与未来展望

文章目录 引言AGI的定义与特征实现AGI的挑战AGI与ASI的区别AGI的潜在影响结语 引言 通用人工智能&#xff08;Artificial General Intelligence, AGI&#xff09;是人工智能领域的终极目标&#xff0c;代表着一种能够执行人类所有智力任务的系统。与当前的任务导向型人工智能&…

[算法] 判断是否为字符串重排(simple, 面试)

文章目录 1. 题意2. 思路3. 编码 好的, 今天我们又是崭新的一天呐, 我们来分享一道很简单的题目 -> 判断是否为字符串重排 因为是简单 面试题的组合, 我们来一步一步走~ 力扣有个题解写的不错, 在这里分享一下: 力扣题解链接 1. 题意 给定两个由小写字母组成的字符串 s1…