选择排序
选择排序的思想是遍历数组,找到最小的一个元素,与第一个位置的元素交换。再遍历数组找到次小的元素,与第二个位置的元素交换位置。重复这个过程,直到全部元素被排序。
选择排序的实现
// 选择排序
void SelectSort(int* a, int n)
{
for (int j = 0; j < n - 1; j++)
{
//一趟排序
int begin = j;
int cur = begin;
for (int i = begin; i < n; i++)
{
if (a[i] < a[cur])
{
cur = i;
}
}
int tmp = a[begin];
a[begin] = a[cur];
a[cur] = tmp;
}
}
堆排序
堆排序的思想是先将数组转换成大堆,取堆顶元素与堆尾元素交换,数组大小减小一个,再对剩下的元素进行向下调整,再次成为一个大堆。再取堆顶元素与堆尾元素交换,重复这个过程,直到所有元素完成排序。
堆排序的实现
// 堆排序
//向下调整——建大堆
void AdjustDwon(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
if (child < n - 1 && a[child + 1] > a[child])
{
child++;
}
//如果子节点比父节点大,则交换
if (a[child] > a[root])
{
int tmp = a[root];
a[root] = a[child];
a[child] = tmp;
root = child;
child = root * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//先转换成大堆
for (int root = (n - 1 - 1) / 2; root >= 0; root--)
{
AdjustDwon(a, n, root);
}
//堆排序
int count = n;
while (count)
{
int tmp = a[0];
a[0] = a[count - 1];
a[count - 1] = tmp;
count--;
AdjustDwon(a, count, 0);
}
}
插入排序
插入排序的思想是取出当前元素,将当前待插入元素与前一个元素比较,如果前一个元素比待插入元素大,则前一个元素的值赋给后一个元素,待插入元素与再前一个元素比较,重复这个过程,直到找到比待插入元素小的元素或者找不到比待插入元素大的元素时,将待插入元素插入到这个元素的后面。这里,如果找不到比待插入元素大的元素时,则待插入元素插入第一个位置。从第二个元素开始,第二个元素以前一个(第一个)元素比较,然后根据情况插入。直到数组最后一个元素都参与了这个插入排序的过程。
插入排序的实现
// 插入排序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
//一趟排序
int right = i;
int cur = a[right + 1];
while (right >= 0)
{
if (cur < a[right])
{
a[right + 1] = a[right];
}
else
{
break;
}
right--;
}
a[right + 1] = cur;
}
}
希尔排序
希尔排序的思想是基于插入排序,因为插入排序在数据有序或者接近有序的情况下具有较高的效率。所以,希尔排序对数据进行预排序,使数据经过预排序后达到接近有序的状态,再在最后进行一次直接插入排序,即可完成排序。
预排序是每次利用插入排序的基本原理,但是待排序元素与前一个元素相隔一段距离gap。这个间隔gap根据数组的大小进行调整。一般情况下每次取gap = n / 3 + 1。这样能够在最开始的时候,将前面的较大的数尽快地排到后面去,而后面的较小的数尽快地排到前面来,但是gap较大排序精度会较低。随着gap的逐渐减小,排序精度逐渐提高。根据表达式gap = n / 3 + 1,最后一次排序是gap = 1的直接插入排序。这样就完成了排序。
希尔排序的实现
// 希尔排序
void ShellSort(int* a, int n)
{
//预排序
int gap = n;
while (gap >= 1)
{
gap /= 3 + 1;
for (int i = 0; i < n - gap; i++) // 这里i++而不使用 i += gap,因为每加gao次就是一个周期
{
int right = i;
int cur = a[right + gap];
while (right >= 0)
{
if (cur < a[right])
{
a[right + gap] = a[right];
}
else
{
break;
}
right -= gap;
}
a[right + gap] = cur;
}
}
}
冒泡排序
冒泡排序的思想是一种交换思想,将第一个元素与第二个元素比较,如果第一个元素比第二个元素大,则交换两个元素的位置。再比较第二个元素与第三个元素,重复这个过程。当比较到最后一个元素时,则确定了最大的元素在最后一个位置。第二次再次比较第一个元素与第二个元素,直到最后一个元素的前一个元素。每比较完一趟就减少一个元素,直到所有元素都完成了冒泡排序。
冒泡排序的实现
// 冒泡排序
void BubbleSort(int* a, int n)
{
for (int j = 1; j < n; j++)
{
for (int i = 0; i < n - j; i++)
{
if (a[i + 1] < a[i])
{
int tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
}
}
}
}
快速排序hoare版本
快速排序基本思想是选定一个key值,一般是选择最左边的元素或者最右边的元素。这里选择最左边的元素。选定key值后,从key值相对的一边开始(右边)向前遍历,当找到比key值小的元素时,停止,再从key值开始向后遍历,当找到比key值大的元素时停止,交换这两个元素。继续这个过程,直到右边的指针与左边的指针相遇(注意,这两个遍历指针一定会相遇)。当相遇时,将key值与相遇时的这个值交换。
完成第一次以上过程后,就找到了一个元素的正确位置,因为在这个元素的左边都是比这个元素小的元素,右边的都是比这个元素大的元素,故这个元素一定在排序后正确的位置上。同时这个过程也划分了左子区间和右子区间,递归地完成左子区间和右子区间。直到子区间为一个元素或为不存在的区间(左值大于右值)时,递归停止。
快速排序hoare版本的实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
int tmp = a[left];
a[left] = a[right];
a[right] = tmp;
}
int tmp = a[left];
a[left] = a[keyi];
a[keyi] = tmp;
keyi = left;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
if (right - left <= 10)
{
InsertSort(a + left, right - left + 1);
return;
}
//int keyi = PartSort1(a, left, right);
//int keyi = PartSort2(a, left, right);
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
快速排序挖坑法
挖坑法是在hoare版本上做了一点改动,具体是选定一个key值(如最左值)并取出,此时key位置为坑,从key值的相对一边开始(最右值),向前遍历当找到一个元素比key值小时,则将这个元素的值赋给坑位置,并将这个元素的位置更新为新的坑。再从key值(左边)开始向后遍历,当找到一个元素比key值大时,将这个值赋给坑位置,在更新坑为这个位置。重复这个过程,当有一边遍历到坑时,停止遍历,将key值赋给坑。
这个过程同样确定了一个元素的正确位置,按照hoare版本的递归思想进行递归即可。
快速排序挖坑法的实现
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int keyi = left;
int tmp = a[keyi];
while (left < right)
{
while (left < right && a[right] >= tmp)
{
right--;
}
a[keyi] = a[right];
keyi = right;
while (left < right && a[left] <= tmp)
{
left++;
}
a[keyi] = a[left];
keyi = left;
}
a[left] = tmp;
keyi = left;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
if (right - left <= 10)
{
InsertSort(a + left, right - left + 1);
return;
}
//int keyi = PartSort1(a, left, right);
//int keyi = PartSort2(a, left, right);
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
快速排序前后指针法
快速排序前后指针法的思想是确定两个指针,第一个是prev指向第一个元素,第二个是cur,指向第二个元素。同样确定一个key值,选择最左值为key值。一趟排序是:从cur开始,如果cur指向的元素比key值小,则交换prev指向的值,prev再加一,cur的值加一。如果cur指向的值比key值大,则prev的值不变,cur加一。一直向后遍历,直到cur完成了最后一个元素的判断。此时,将key值与prev指向的值交换。
这个过程是一趟排序。确定了一个元素的正确位置,划分了区间。按照上述的递归思想,递归地完成排序即可。
快速排序前后指针法的实现
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
int tmpval = a[left];
a[left] = a[mid];
a[mid] = tmpval;
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
int tmp = a[cur];
a[cur] = a[prev];
a[prev] = tmp;
}
cur++;
}
int tmp = a[keyi];
a[keyi] = a[prev];
a[prev] = tmp;
keyi = prev;
return keyi;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
{
return;
}
if (right - left <= 10)
{
InsertSort(a + left, right - left + 1);
return;
}
//int keyi = PartSort1(a, left, right);
//int keyi = PartSort2(a, left, right);
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
注意,在以上代码中,存在快速排序的小区间优化。需要看出,当递归进行到最后几层时,递归的次数是最多的。所以在代码中,当只剩下最后10个元素时,不再递归,而是直接插入排序。能够这么做的是因为当快速排序进行到这个阶段时,虽然区间里的数据没有完全有序,但是已经接近有序,而插入排序能够很好地适应郑重情况。
最后,快速排序还有一个关键的问题,那就是快速排序依赖key值的选取,如果key值选取地不是很恰当,会影响到快速排序的效率。如:数据已经是有序的情况,选择最左值为key值,会导致O(n^2)的时间复杂度。为了解决这个问题,引入了三数取中法。使用这个方法能够很好地避免最坏的情形。
三数取中法
//快速排序3数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[right])
{
if (a[left] > a[mid])
{
return left;
}
else if (a[right] < a[mid])
{
return right;
}
else
{
return mid;
}
}
else
{
if (a[left] < a[mid])
{
return left;
}
else if (a[right] > a[mid])
{
return right;
}
else
{
return mid;
}
}
}
快速排序的非递归实现
快速排序的基本实现是使用递归,极端情况下,当递归深度太深时会出现栈溢出。同时掌握递归转非递归也是一种基本能力。其主要思想是使用栈模拟递归,这样来完成快速排序的非递归实现。
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack* ps = StackInit();
StackPush(ps, right);
StackPush(ps, left);
while (!IsEmpty(ps))
{
left = StackTop(ps);
right = StackTop(ps);
if (left >= right)
{
continue;
}
if (right - left <= 10)
{
InsertSort(a + left, right - left + 1);
continue;
}
int keyi = PartSort3(a, left, right);
StackPush(ps, right);
StackPush(ps, keyi + 1);
StackPush(ps, keyi - 1);
StackPush(ps, left);
}
StackDestroy(ps);
}
注意,这里引用了模拟实现的栈,因为C语言需要自行实现栈的相关功能。这里模拟使用栈在堆上开辟了空间,所以记得释放内存,避免内存泄漏。
归并排序
归并排序需要开相同数量的元素空间,用于归并排序,然后再拷贝到原数组中。首先将数据区间分成两部分,一般取中点。如果这两个子区间是可以进行归并排序的(有序的),则进行归并排序。如果这两个子区间不是能够进行归并排序的(无序的),则分别对两个子区间分割,再考察对应的更小的两个子区间是否能够进行归并排序。递归这个过程,直到可以进行归并排序的最小单元——两个由一个元素组成的子区间,这样就能进行归并排序了。
而归并排序是这样的:两个有序的区间分别从第一个元素开始比较,如果其中一个元素更小,则这个元素加入到开辟出来的那个数组中的第一个位置。重复这个过程找到次小的元素加入到开辟出来的数组中的第二个位,直到有一个区间里的元素全部加入了,则把另一个区间的剩余元素全部加入到开辟出来的数组里。
最后拷贝这个归并排序好的数据到原数组中。
归并排序的实现
//归并排序主要功能实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2;
//如果子区间不能归并排序,则递归
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
//归并排序
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = 0;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memmove(a + begin, tmp, sizeof(a[0]) * (end - begin + 1));
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
int* tmp = (int*)malloc(sizeof(a[0]) * n);
if (tmp == NULL)
{
perror("malloc");
exit(-1);
}
_MergeSort(a, begin, end, tmp);
free(tmp);
}
归并排序的非递归实现
这里非递归是直接改为循环实现,选择一个gap作为每次归并的区间大小,gap从1开始代表从只有一个元素的区间开始归并。所以最内层循环是处理两两区间的归并,如:[0,0]和[1,1]、[2,2]和[3,3]等等。最外层循环是决定gap大小的,即区间的宽度。全进宽度从1(单个元素)到2、4、8……按照递归的逆向进行。将两个由1个元素组成的区间归并为一个两个元素的区间,再将两个有2个元素组成的区间归并为一个4个元素的区间,如此进行下去。最终的gap值不能超过元素个数。即gap <= 元素个数n。
而每次循环都进行归并,特别注意,区间的边界是十分重要的,按照二分的思想进行区间的划分,可能会导致最右边的边界出现越界的情况,每次循环都需要对区间进行判断,如果越界就要进行修改。具体解决方法在代码中展示。
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(a[0]) * n);
if (tmp == NULL)
{
perror("malloc");
exit(-1);
}
for (int gap = 1; gap < n; gap *= 2)
{
for (int begin = 0; begin < n; begin += 2 * gap)
{
int begin1 = begin;
int end1 = begin1 + gap - 1;
int begin2 = begin1 + gap;
int end2 = begin2 + gap - 1;
int i = 0;
//区间修正
if (end1 >= n)
{
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
end2 = n - 1;
}
//归并排序
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memmove(a + begin, tmp, sizeof(a[0]) * (end2 - begin + 1));
}
}
free(tmp);
}
记得释放开辟的内存,避免内存泄漏。
一个有趣的排序
计数排序
计数排序的思想是开辟足够的空间,在数组与开辟的空间之间建立一个映射,开辟的空间一般是用数组中最大值减最小值加1,对开辟的元素都赋初值为0。因此这个映射是一对一的。这样,循环遍历数组,每当遇到一个数,则这个数对应的那个元素加1。直到完成遍历。然后再顺序地循环遍历开辟的元素,每当遇到一个元素不为0,则在原数组写入这个开辟的元素对应的原数组数据。具体实现如代码所示。
// 计数排序
void CountSort(int* a, int n)
{
int min = a[0];
int max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
else if(a[i] > max)
{
max = a[i];
}
}
int* tmp = (int*)malloc(sizeof(int) * (max - min + 1));
if (tmp == NULL)
{
perror("malloc");
exit(-1);
}
memset(tmp, 0, sizeof(int) * (max - min + 1));
for (int i = 0; i < n; i++)
{
tmp[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < max - min + 1; i++)
{
while (tmp[i]--)
{
a[j++] = i + min;
}
}
}