在学习数据结构中我感觉最重要的就是排序算法,以下我列举最常见的几种排序算法及他们各自的思想
接下来我一一介绍这几种排序算法:
1.直接插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一
个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
代码实现:
void InsertSort(int *array, int size)
{
//直接从第二个元素开始,因为第一个元素已经有序
for (int i = 1; i < size; i++)
{
int key = array[i];//key为待排元素
int end = i - 1;//end为比较元素
while (end >= 0 && key < array[end])
{
array[end + 1] = array[end];//把end位置的元素放到end+1的位置
end--;
}
array[end + 1] = key;
}
}
性质:
- 适用场景:元素接近有序,直接插入排序算法的时间效率越高
- 空间复杂度:o(n^2)
- 时间复杂度:o(1)
- 稳定性:稳定
2.希尔排序
希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序,因D.L.Shell于1959年提出而得名。希尔排序法的基本思想是:选定一个整数,把待排序文件中所有元素按整数的间隔分组,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。每次整数减一,当整数到达1时,这时所有记录在统一组内排好序。
代码实现:
void ShellSort(int *array, int size)
{
//gap作为分组的依据
int gap = size;
while (gap > 1)
{
gap = gap / 3 + 1;//
for (int i = gap; i < size; i++)
{
int key = array[i];
int end = i - gap;
while (end >= 0 && key < array[end])
{
array[end + gap] = array[end];
end -= gap;
}
array[end + gap] = key;
}
}
}
性质:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 平均时间复杂度: O(N1.3—N2)
- 空间复杂度:o(n)
- 稳定性:不稳定
3.选择排序
选择排序的基本思想是:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,用剩余的数据和其相比较,每次找到数据中最大或者最小的元素,循环进行直到全部待排序的数据元素排完 。
代码实现:
void SelectSort(int *array, int size)
{
for (int i = 0; i < size-1; i++)//控制循环的趟数
{
int maxpos = 0;
for (int j = 1; j < size - i; j++)
{
if (array[j] > array[maxpos])//比较j和maxpos位置元素的大小,看maxpos位置是否合适
{
maxpos = j;
}
}
if (maxpos != size - 1 - i)//第一次比较结束时,判断maxpos是不是在末尾
{
swap(&array[maxpos], &array[size - 1 - i]);
}
}
}
性质:
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
上边的选择排序:一次只可以找出一个最大或者最小的元素,效率比较低,所以对选择排序进行优化,代码如下:
//选择排序的优化
void SelectSortMax(int *array, int size)
{
int begin = 0;
int end = size - 1;//找到元素的起始和末尾
while (begin < end)
{
int minpos = begin;
int maxpos = begin;
int indx = begin + 1;//待比较元素
while(index <= end)
{
if (array[index]>array[maxpos])//如果待比较元素比最大的元素还大,说明最大的元素位置不合理
{
maxpos = index;
}
if (array[index] < array[minpos])//如果待比较元素比最小的元素还小,说明最小的元素位置不合理
{
minpos = index;
}
index++;
}
if (maxpos != end)//比较结束后判断maxpox位置是否在末尾
{
swap(&array[maxpos], &array[end]);
}
if (minpos == end)//这行代码是判断最小元素的位置是不是在队列末尾,如果在,不能直接进行交换,因为上一步放置maxpos位置时已经进行交换,所以我们先把minpos的位置放在上一步交换之前maxpos的位置,然后再判断minpos的位置是否在起始
{
minpos = maxpos;
}
if (minpos != begin)//比较结束后判断minpos位置是否在起始
{
swap(&array[minpos], &array[begin]);
}
begin++;
end--;
}
}
4.堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。注意:排升序要建大堆,排降序建小堆。
代码实现:
void HeapSort(int *array, int size)
{
//使用堆排序首先要进行建堆
//root为(size-2)>>1的原因:
//size-1就是队列中最后一个元素的位置,也就是最后一个孩子的位置
//在二叉树中:child = parent*2 +1;所以最后一个父节点位置:(child-1)>>1
for (int root = (size - 2) >> 1; root >= 0; root--)
{
AdjustHeap(array, size, root);
}
//开始排序
int end = size - 1;
while (end)
{
//因为升序建立的是大堆,所以将堆顶元素直接和堆底元素进行交换
int temp = array[0];
array[0] = array[end];
array[end] = temp;
//交换完毕后,调整现在的堆顶元素
AdjustHeap(array, end, 0);
end--;
}
}
void AdjustHeap(int *array, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && array[child] < array[child + 1])
{
child += 1;
}
if (array[child] > array[parent])
{
int temp = array[parent];
array[parent] = array[child];
array[child] = temp;
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
性质:
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
5.冒泡排序
冒泡排序的基本思想是:对所有相邻记录的关键字值进行比效,每次找到数列中一个最大或者最小的元素。
代码实现:
void BubbleSort(int *array, int size)
{
//n个元素只用比较n-1次,最后剩下一个元素不用进行比较
for (int i = 0; i < size - 1; i++)
{
//j < size-i-1理解:
//j < size-i:待比较元素,没比较一次就有一个元素已经有序
//j < size-i-1:因为j是和j+1进行比较的,所以还要保证j+1合法
for (int j = 0; j < size - i - 1; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
性质:
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
6.快速排序
快速排序是这几种算法中比较难理解的一种,快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其**基本思想是:**任取待排序元素序列中的某元素作为基准值(一般取末尾元素),按照该排序码将待排序集合分割成两子序列,从左往右找基准值大的,从右往左找比基准值小的,进行交换,交换完成后,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序有三种实现方法:1.1. hoare版本 2. 挖坑法 3. 前后指针版本;以下我在代码中讲解这三种方法:
//三数取中法
int GetMid(int *array, int left, int right)
{
int mid = left + (right - left) >> 1;
//首先判断left和right-1位置的元素的大小
//然后再用mid位置元素的大小和他们相比较,找出三数中间位置的元素
if (array[left] < array[right - 1])
{
if (array[mid] < array[left])
return left;
else if (array[mid] > array[right - 1])
return right - 1;
else
return mid;
}
else
{
if (array[mid] < array[right - 1])
return right - 1;
else if (array[mid]>array[left])
return left;
else
return mid;
}
}
//1.hoare版本:
int Partion1(int *array, int left, int right)
{
int begin = left;
int end = right - 1;
//三数取中是为了优化最差情况
//最差情况:会存在一种情况,每次划分数据都存在在基准值一侧,所以我们采用三数取中来尽量避免
int mid = GetMid(array, left, right);
swap(&array[mid], &array[right - 1]);
int key = array[end];//这时候的key就是偏向中间的数
while (begin < end)
{
//从左往右找比基准值大的
while (begin < end && array[begin] <= key)
{
begin++;
}
//从右往左找比基准值小的
while (begin < end && array[end] >= key)
{
end--;
}
if (begin < end)
{
swap(&array[begin], &array[end]);
}
}
if (begin != right - 1)
{
swap(&array[begin], &array[right-1]);
}
return begin;
}
//2.挖坑法
//此方法就是通过begin和end位置的元素不断交换来找到基准值的位置
void Partion2(int *array, int left, int right)
{
int begin = left;
int end = right - 1;
int mid = GetMid(array, left, right);
swap(&array[mid], &array[right - 1]);
int key = array[end];
while (begin < end)
{
//从左往右找比基准值大的
while (begin < end && array[begin] <= key)
{
begin++;
}
//找到之后,就把begin位置的元素放置到end的位置,同时end向中间走一步
if (begin < end)
array[end--] = array[begin];
//从右往左找比基准值小的
while (begin < end && array[right] >= key)
{
end--;
}
//找到之后,就把end位置的元素放置到begin位置,同时begin向中间走一步
if (begin < end)
array[begin++] = array[end];
}
//最后跳出循环,begin和end相遇,也就是基准值的位置
array[begin] = key;
return begin;
}
//3.前后指针法
void Partion3(int *array, int left, int right)
{
int cur = 0;
int prev = cur - 1;
int mid = GetMid(array, left, right);
swap(&array[mid], &array[right - 1]);
int key = array[right - 1];
while (cur < right)
{
//if条件不满足,只给cur++;这时cur和prev就不是前后指针关系
//当if条件满足,则证明array[cur]<key,就说明找到了第一个比基准值小的元素
//这时候就把prev和cur位置的元素交换,将小的元素放在左边
if (array[cur] < key && ++prev != cur)
{
swap(&array[prev], &array[cur]);
}
cur++;
}
//判断prev的下一步是不是在末尾,如果不是就把基准值放置在prev位置
if (++prev != right - 1)
{
swap(&array[prev], &array[right - 1]);
}
return prev;
}
//快速排序
void QuickSort(int *array, int left ,int right)
{
if (right <= left)
{
return;
}
else
{
//区间是左闭右开
int div = Partion1(array, left, right);
Quick(array, left, div);
Quick(array, div + 1, right);
}
}
性质:
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
7.归并排序
归并排序的**基本思想是:**归并排序(MegreSort)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
代码实现:
void MegreData(int *array, int *temp, int left, int mid, int right)
{
//区间是左闭右开的
int begin1 = left, end1 = mid;
int begin2 = mid, den2 = right;
int index = left;
//将begin1和begin2区间中的元素放置到新空间中
while (begin1 < end1 && begin2 < end2)
{
if (array[begin1] <= array[begin2])
temp[index++] = array[begin1++];
else
temp[index++] = array[begin2++];
}
//证明begin2中的元素已经放置完毕
while (begin1<end1)
{
temp[index++] = array[begin1++];
}
//证明begin1中的元素已经放置完毕
while (begin2<end2)
{
temp[index++] = array[begin2++];
}
}
void _MegreSort(int *array, int *temp, int left, int right)
{
if (right - left>1)
{
int mid = left + (right - left) >> 1;
_MegreSort(array, temp, left, mid);
_MegreSort(array, temp, mid, right);
//合并数据
MegreData(array, temp, left, mid, right);
//加left的原因是,每次都要往新空间中去拷贝,并且每次拷贝的元素都不是从头开始,因为有递归存在
memcpy(array + left, temp + left, (right - left)*sizeof(array[0]));
}
}
void MegreSort(int *array, int size)
{
int temp = (int*)malloc(sizeof(array[0])*size);
if (temp == NULL)
{
assert(0);
return;
}
_MegreSort(array, temp, 0, size);
free(temp);
}
性质:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定