排序算法评判的标准
1.时间复杂度
2.空间复杂度
3.稳定性:在整个排序过程中,有没有跳跃式的交换数据。如果有,则排序算法不稳定,如果没有,则排序算法是稳定的
辅助方法代码
void ShowArr(int* arr, int len) //显示数组
{
for (int i = 0; i < len; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
}
bool IsSort(int* arr, int len) //判断数据是否有序,从小到大
{
for (int i = 0; i < len - 1; ++i)
{
if (arr[i] > arr[i + 1])//前一个比后一个大
return false;
}
return true;
}
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
排序算法的稳定性:在整个排序过程中,有没有跳跃式的交换数据。如果有,则排序算法不稳定,如果没有,则排序算法是稳定的
一趟排序的过程,将相邻的两个元素进行比较,如果前一个比后一个大,则将两个元素交换–将最大的元素交换到整个数据的最后。
排序的趟数:数据元素的个数:len - 1
/*
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
*/
void BubbleSort1(int* arr, int len)
{
for (int i = 0; i < len - 1; ++i)
{
//一趟循环结束后,最大的数据被排到最后
for (int j = 0; j < len - 1 - i; ++j)//len - 1 - i 每经过一趟排序,下一次就可以少比较一个数据
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
}
}
}
}
void BubbleSort(int* arr, int len)//优化后
{
for (int i = 0; i < len - 1; ++i)
{
bool flag = true;
for (int j = 0; j < len - 1 - i; ++j)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
flag = false;
}
}
//一趟循环走完后,flag还为真,说明本来数据就有序,没有进行交换
if (flag)
return;
}
}
1.先遍历一遍整个待排序数据,找到当前最大的值的位置。
2.将标记的最大值与当前最后一个元素交换。
3.重复上述过程,直到只剩下一个数据
上面的步骤每做一次,下一次就少遍历一个数据。(本次找到的最大值)
选择排序执行的趟数:数据元素的个数 len - 1
/*
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定
*/
void SelectSort(int* arr, int len)
{
for (int i = 0; i < len - 1; ++i)
{
int max_index = 0; //最大值的下标
for (int j = 0; j < len - i; ++j)
{
if (arr[j] > arr[max_index])
{
max_index = j;
}
}
Swap(&arr[max_index], &arr[len - 1 - i]); //将标记的最大值与当前最后一个元素交换
}
}
1.将数据分为两部分,前一段是已经有序的数据(初始时,前一段数据只有一个),后一段是无序数据。
2.从无序数据中拿一个数据(拿左边的第一个数据),插入到前一段的有序数据中,并且使其依旧有序。
3.重复上述过程,直到无序数据段没有数据。
/*
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定
*/
void InsertSort(int* arr, int len)
{
for (int i = 1; i < len; ++i) // i 负责遍历无序数据段
{
int tmp = arr[i];
int j = 0;
for (j = i - 1; j >= 0; --j)
{
if (arr[j] <= tmp)
break;
arr[j + 1] = arr[j];
}
arr[j + 1] = tmp;
}
}
直接插入排序分析进阶:
思考:如果数据已经有序,使用直接插入排序,时间复杂度是多少?
如果数据已经有序,那么直接插入排序的内层循环就是一个常语句,所以时间复杂度就为O(n)
结论:数据越有序,直接插入排序速度越快,时间复杂度趋近于O(n)。
1.先将数据分成 d 个组,在每个分组内使用直接插入排序算法进行排序。目的就是让整个数据序列越来越有序
2.接着将数据继续分组(分组数越来越小),然后排序,最后一次分组肯定为 1 组。每次分组的组数一般是互质的。
void Shell(int* arr, int len, int group)
{
for (int i = group; i < len; ++i) // i 负责遍历整个数据段,控制本次直接插入排序处理那一组的数据
{
int tmp = arr[i];
int j = 0;
for (j = i - group; j >= 0; j -= group)
{
if (arr[j] <= tmp)
break;
arr[j + group] = arr[j];
}
arr[j + group] = tmp;
}
}
/*
时间复杂度:O(n^1.3 ~ n^1.5) 数学推导得出
空间复杂度:O(1)
稳定性:不稳定
*/
void ShellSort(int* arr, int len)
{
int group[] = {
5, 3, 1}; //分组没有标准的分法,一般组数我们自己设置,数据量大我们可以自己加
for (int i = 0; i < sizeof(group) / sizeof(group[0]); ++i)
{
Shell(arr, len, group[i]);
}
}
二叉树的概念:二叉树(binary tree)是指树中节点的度不大于2的有序树。在二叉树中除了根节点的每一个节点都仅有一个父节点,所有的节点最多只能有两个孩子节点。
(1)除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。
(2)国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
(3)国外(国际)定义:a binary tree T is full if each node is either a leaf or possesses exactly two childnodes.
大意为:如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。(一棵满二叉树的每一个结点要么是叶子结点,要么它有两个子结点,但是反过来不成立,因为完全二叉树也满足这个要求,但不是满二叉树) ||不够准确
对满二叉树的结点进行编号, 约定编号从根结点起, 自上而下, 自左而右。则深度为k的, 有n个结点的二叉树, 当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时, 称之为完全二叉树
堆通常是一个可以被看做一棵完全二叉树。
a.大根堆,就是说这个完全二叉树中的每一棵子树的根节点都大于它的左右孩子(如果有的话)
b.小根堆:就是说这个完全二叉树中的每一棵子树的根节点都小于它的左右孩子(如果有的话)
(1)顺序存储
(2)链式存储:每个节点都有至少三个域(data,leftchild,rightchild)
堆仅仅将数字存储在数组中
1.如果父节点的下标为 i ,则左孩子的下标为 2*i+1,右孩子的下标为2*i+2
2.如果孩子的下标为 j ,则父节点的下标为 (j - 1)/2
1.将数组中的数据构造成堆时,从最后一棵子树开始,想根节点一棵棵调整。
2.每棵子树的调整都是从其根节点开始向下调整的。
一棵子树的调整过程
先用 i 指向子树的根节点, j 指向根节点的左孩子。将 i 位置的值保存到临时变量(tmp)中,**然后在左右孩子中找到较大的那一个,j 指向较大值,(1)如果较大的那一个比临时变量中的值小,则直接退出。(2)如果较大的那一个比临时变量值大,则将较大的值保存到当前的 i 位置上,然后指向父节点的变量就为较大值的下标(即 i = j,j = 2*i+1)。直到左孩子的下标( j )越界退出。退出以后,将tmp的值保存退出时的父节点位置。
代码实现:
//O(log(n))
void OneAdjust(int* arr, int len, int root)
{
int tmp = arr[root];
int i = root;
int j = 2 * i + 1;
while (j < len)
{
if (j + 1 < len && arr[j] < arr[j + 1]) // j + 1 < len 成立,则有右孩子。arr[j] < arr[j+1]左孩子小于右孩子
j++;
if (arr[j] < tmp)
break;
arr[i] = arr[j];
i = j;
j = 2 * i + 1;
}
arr[i] = tmp;
}
将数组调整成堆的过程
先找到最后一棵子树的根节点,向根节点递减,循环调用上面的调整方法。
代码实现:
//O(nlog(n))
void CreateHeap(int* arr, int len)
{
int lastRoot = (len - 2) / 2;//((len - 1) - 1) / 2 /*lastRoot就是最后一棵子树的根节点下标
for ( int i = lastRoot; i >= 0; --i) //从lastRoot开始,循环到整棵树的根节点,调整
{
OneAdjust(arr, len, i);
}
}
先将数组中的数据调整成一棵最大堆,然后将根节点的数据和最后位置的节点交换,接着除过最后一个位置的数据外,再将剩余的数据调整成最大堆。
重复上述过程,直到只剩下一个数据没有交换。就完成了堆排序。
代码实现:
/*
时间复杂度:O(nlog(n))
空间复杂度:O(1)
稳定性:不稳定
*/
void HeapSort(int* arr, int len)
{
CreateHeap(arr, len);
for (int i = 0; i < len - 1; ++i)
{
Swap(&arr[0], &arr[len - 1 - i]);
OneAdjust(arr, len - i - 1, 0);
}
}
扩展应用:
1.找倒数第 k 大或者倒数第 k 小的数据。
2.只需要关注最小的或者最大的值的场景。
思考的两个问题:
- 处理一段数据时,什么样的情况下就认为这一段数据已经完全有序。
- 如何将数据按照基准分成两部分。
一、按照一个基准数据,将待排序数据段分成两部分
数据段为 start 到 end 之间的数据。先将基准数据(start 位置)保存到 tmp 中,i = start ,j = end 。在 i < j 的时候,重复执行以下两个过程。
- 通过 j 从后往前找第一个比基准数据小的数据,找到后就将 j 位置的值赋值到 i 位置。
- 通过 i 从前往后找第一个比基准数据大的数据,找到后就将 i 位置的值赋值到 j 位置。
循环退出( i == j )后,将 tmp 中的基准数据存储在 i 位置。此时,数据就被分成了前后两部分。
代码:
//根据基准值,找到基准数据应该所在的位置,将数据在原始的空间中划分成两部分
int OneQuick(int* arr, int start, int end)
{
int tmp = arr[start]; //基准数据
int i = start, j = end;
while (i < j)
{
//从后往前找比基准大的数据
while (i < j && arr[j] > tmp) j--;
arr[i] = arr[j];
//从前往后找比基准小的数据
while (i < j && arr[i] <= tmp) i++;
arr[j] = arr[i];
}
arr[i] = tmp;
return i;
}
二、处理前一段和后一段数据 — 递归
判断一段数据是否已经处理完的依据就是看这个段中还有几个数据,只要超过一个数据,那就认为并没有处理完成。
代码:
/*
时间复杂度:O(nlog(n))
空间复杂度:O(log(n))
稳定性:不稳定
*/
void Quick(int* arr, int start, int end) //O(nlog(n))
{
//退出的条件
if (end - start < 1) return;
else
{
int mid = OneQuick(arr, start, end);
Quick(arr, start, mid - 1); //递归处理前一段数据
Quick(arr, mid + 1, end); //递归处理后一段数据
}
}
void QuickSort(int* arr, int len)
{
Quick(arr, 0, len - 1);
}
将Quick方法改为非递归(循环实现)。但是循环体中一次只能处理一部分数据,那剩余部分数据的 start,end 这一对数据就需要通过其他的数据结构(栈和队列)将其保存。
结构声明
将要存储的数据段的起始位置(start)和结束位置(end)封装成(struct)一个数据。
typedef struct
{
int start;
int end;
}ElemType;//这个类型就是将来要给栈或者队列中插入的数据。
//空间复杂度:O(log(n)) 栈使用的空间决定的
//非递归实现
void Quick(int* arr, int start, int end)
{
Stack st;
InitStack(&st);
ElemType val = {
start, end };
Push(&st, val); //将整个数据段的起始和结束位置Push到栈中
while (!Empty(&st))
{
ElemType data;
Top(&st, &data);
Pop(&st);
int mid = OneQuick(arr, data.start, data.end);
if (mid - data.start > 1) //mid - 1 - data.start > 0 //基准的前一个数据减起始位置的数据,只剩一个数据说明这段数据处理完了(已经有序)
{
val.start = data.start;
val.end = mid - 1;
Push(&st, val); //mid这个基准数据左边有超过1个的数据压栈
}
if (data.end - mid > 1) //data.end - (mid + 1) > 0 //后一段数据
{
val.start = mid + 1;
val.end = data.end;
Push(&st, val); //mid这个基准数据右边有超过1个的数据压栈
}
}
DestroyStack(&st);
}
如果在一个工程中使用了其他工程的代码
1.设置工程的属性,在工程中引入其他的工程。将要引入的工程的配置类型改为静态库。
2.在使用另一个工程的代码的源文件中直接使用 inculde 引入另一个工程的.cpp文件 (一般不使用)
6.4 快速排序的优化
1.用随机数来选取基准数据。
2.用三位数取中法(第一个数,最后一个数和中间那个数的中位数)选取基准数据
3.当代排序序列数据量达到一定数量时,使用插入排序
4.聚集相等元素
一次排序过程,将已经各自有序的两个段的数据合并成一个段,并且合并后依旧有序。
第一次我们认为单个数据是有序的,一个数据一个段,一次排序后,两个数据就是一个有序数据段,这样下一次每个有序数据段就有两个数据,最后将其合并成一个完全有序段,则整个数据就已经排序好了。
void Meger(int* arr, int len, int width, int* brr)
{
int low1 = 0;
int high1 = low1 + width - 1; //high1为第一个段的结束位置下标
int low2 = high1 + 1;
int high2 = low2 + width > len ? len - 1 : low2 + width - 1;
int* brr = (int*)malloc(sizeof(int) * len);
assert(brr != NULL);
int index = 0;
//处理有两个归并段
while ( low2 < len)
{
//两个归并段都有未归并数据
while (low1 <= high1 && low2 <= high2)
{
if (arr[low1] < arr[low2]) brr[index++] = arr[low1++];
else brr[index++] = arr[low2++];
}
//只剩下一个归并段数据,将两个归并段剩余的数据复制到brr中
//只会执行其中一个while循环
while (low1 <= high1) brr[index++] = arr[low1++];
while (low2 <= high2) brr[index++] = arr[low2++];
low1 = high2 + 1;
high1 = low1 + width - 1;
low2 = high1 + 1;
high2 = low2 + width > len ? len - 1 : low2 + width - 1;
}
//处理只剩下一个归并段的情况
while (low1 < len) brr[index++] = arr[low1++];
//将brr中的数据全部复制回arr中
for (int i = 0; i < len; i++) arr[i] = brr[i];
}
void MegerSort(int* arr, int len)
{
int* brr = (int*)malloc(sizeof(int) * len);
assert(brr != NULL);
// i 就是每个段当前的数据个数
for (int i = 1; i < len; i *= 2)
{
Meger(arr, len, i, brr);
}
free(brr);
}
针对于有多个关键字的排序算法,先按照一个关键字排序,完成后再按照另一个关键字排序。先按照权重小的关键字排序,再按照权重大的关键字排序。
按照每个关键字值的范围,定义相同数量的队列。对于数字我们先按照个位进行排序,然后再按照十位进行排序
//获取最大数据的位数
int GetDigit(int* arr, int len)
{
int max = arr[0];
for (int i = 0; i < len; i++)
{
if (max < arr[i]) max = arr[i];
}
int digit = 0;
while (max)
{
digit++;
max /= 10;
}
return digit;
}
//获取一个数据相应位数上的值
int GetRadix(int val, int digit)
{
//val 1234
int radix = val % 10; //4
while (digit)
{
val /= 10; //123
radix = val % 10; //3
digit--;
}
return radix;
}
/*
时间复杂度:O(d*n) d是关键字的个数
空间复杂度:O(n)
稳定性:稳定
*/
void RadixSort(int* arr, int len)
{
int maxDigit = GetDigit(arr, len);
ListQue que[10];
for (int i = 0; i < 10; i++)
{
InitListQue(&que[i]);
}
//根据不同的位数,处理整个数据
for (int i = 0; i < maxDigit; i++)
{
//将arr中的所有数据取其 i 这个位数的值,并且插入到相应值的队列中
for (int j = 0; j < len; j++)
{
int radix = GetRadix(arr[j], i);
Push(&que[radix], arr[j]);
}
//将所有的队列按照从前到后的顺序把值全部出到arr中
int index = 0;
for (int k = 0; k < 10; k++)
{
while (!Empty(&que[k]))
{
GetHead(&que[k], &arr[index++]);
Pop(&que[k]);
}
}
}
for (int i = 0; i < 10; i++)
{
DestroyListQue(&que[i]);
}
}