000000000000
前言
> 首先,我们来介绍一下排序的常见概念排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序
一、插入排序
1.原理
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
2.代码实现
代码如下(示例):
void InsertSort(int * arr, int len)
{
int i = 0;
int j = 0;
for (i = 1; i < len; i++)
{
int key = arr[i];
j = i-1;
while (j >= 0 && arr[j] > key)
{
arr[j+1] = arr[j];
j--;
}
arr[j+1] = key;
}
}
3.时间复杂度
插入排序排第一个数最多遍历1次,第二个数最多2次… 第n个数最多n次即为逆序的情况
1 + 2 + 3 + … + n = n (1 + n) / 2
O(N2)
4. 空间复杂度
未借助辅助空间
O(1)
5.稳定性
稳定
6.应用场景
适用于元素数量比较少或元素接近有序
二、希尔排序
1.原理
先选定一个整数即gap,把待排序文件中所有记录分成个组,并对每一组内的记录进行直接插入排序.然后,缩小gap值,重复上述分组和排序的工作。当gap为1时,即为对整个序列应用直接插入排序.
2.代码实现
代码如下(示例):
void ShellSort(int * arr, int len)
{
int gap = len;
gap = gap / 3 + 1; //取值问题?
int k = 0;
int i = 0;
int j = 0;
for (k = gap; gap > 0; gap--)
{
for (i = gap; i < len; i++)
{
j = i - gap;
int key = arr[i];
while (j >= 0 && arr[j] > key)
{
arr[j+gap] = arr[j];
j -= gap;
}
arr[j+gap] = key;
}
}
}
3.时间复杂度
gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(n^1.25)到O(1.6 n^1.25)到来算。
4. 空间复杂度
未借助辅助空间
O(1)
5.稳定性
不稳定
6.应用场景
适用于任然使用插入排序,元素不是接近有序或数量大的情况下
三、选择排序
1.原理
每一次从待排序的数据元素中选出最大(或最小)的一个元素,存放在序列的最后一个位置,直到全部待排序的数据元素排完 .
2.代码实现
代码如下(示例):
代码一
void SelectSort(int * arr, int len)
{
int i = 0;
int j = 0;
for (i = 0; i < len-1; i++) //趟数
{
int maxIndex = 0;
for (j = 1; j < len-i; j++) //找到最大值
{
if (arr[j] > arr[maxIndex])
{
maxIndex = j;
}
}
if (maxIndex != len-1-i)
{
swap(&arr[maxIndex], &arr[len-1-i]); //把最大元素放到末尾
}
}
}
代码二是对代码一的优化, 既然每次可以找到最大值放到末尾,我们为何不每趟找到最大值放到末尾,最小值放到开头,每次处理两个数,减少趟数
代码二
void SelectSort1(int * arr, int len)
{
int begin = 0;
int end = len-1;
while (begin < end)
{
int maxIndex = begin; //存放最大值的下标
int minIndex = begin; //存放最小值的下标
int l = begin + 1;
while (l <= end)
{
if (arr[l] > arr[maxIndex])
{
maxIndex = l;
}
if (arr[l] < arr[minIndex])
{
minIndex = l;
}
l++;
}
//为了解决最小值在末尾的问题
if (end == minIndex)
minIndex = maxIndex;
if (maxIndex != end)//最大值与末尾元素交换
{
swap(&arr[maxIndex], &arr[end]);
}
if (minIndex != begin)//最小值与开头元素交换
{
swap(&arr[minIndex], &arr[begin]);
}
begin++,end--;
}
}
这条判断语句是必要的,没有会出现问题
倘若最小元素出现在末尾
找到最大的元素和最小的元素
将最大的元素与末尾元素交换,但此时最小的元素恰好在末尾,min指向的不在时最小的元素了
然后我们把min指向的元素与开头元素交换,结果最大的元素被交换了2次,
返现结尾不是最大的元素,开头不是最小的元素
3.时间复杂度
O(N2)
4. 空间复杂度
O(1)
5.稳定性
不稳定
6.应用场景
接近有序的序列(很少使用)
四、堆排序
1.原理
- 建堆
升序:建大堆
降序:建小堆 - 利用堆删除思想来进行排序(将堆顶元素与堆中最后一个元素交换,然后将堆顶元素向下调整,继而堆顶元素与堆中倒数第二个元素交换,继续向下调整堆顶元素…依次类推,直至堆中堆顶元素没有要交换的元素)
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
2.代码实现
代码如下(示例):
//向下调整
void AdjustDown(int * arr, int len, int index)
{
int father = index; //双亲节点
int child = father*2 + 1; //孩子节点
while (child < len)
{
if (child + 1 < len && arr[child+1] > arr[child]) //找到左右孩子最大的
{
child += 1;
}
if (arr[father] < arr[child])
{
swap(&arr[father], &arr[child]);
father = child;
child = child*2 + 1;
}
else
{
return;
}
}
}
//建立大顶堆
void CreateHeap(int * arr, int len)
{
int child = (len-2) / 2;//非叶子节点的最后一个双亲节点
int i = child;
for (; i >= 0; i--)
{
AdjustDown(arr, len, i);
}
}
//堆排序
void HeapSort(int * arr, int len)
{
CreateHeap(arr, len);
int i = len-1;
while (i > 0)
{
swap(&arr[0], &arr[i]);
AdjustDown(arr, i, 0);
i--;
}
}
3.时间复杂度
O(NlogN)
4. 空间复杂度
O(1)
5.稳定性
不稳定
6.应用场景
Top K的问题
堆hp中建立了一个大堆(小堆),如果数组arr剩余的元素小于(大于)堆顶元素,则交换,然后堆顶元素向下调整.
五、冒泡排序
1.原理
冒泡排序每一趟都是将数字两两比较,如果不满足情况则交换,保证将最大(小)的数冒到末尾
2.代码实现
代码如下(示例):
void BubbleSort(int * arr, int len)
{
int flag = 0;
int i = 0;
int j = 0;
for (i; i < len-1; i++) //趟数 len-1
{
for (; j < len-1-i; j++)
{
if (arr[j] > arr[i+1])
{
swap(&arr[j], &arr[j+1]);
flag = 1;
}
if (!flag)//元素有序,不需要再排序
{
break;
}
}
}
}
3.时间复杂度
O(N2)
4. 空间复杂度
O(1)
5.稳定性
稳定
6.应用场景
接近有序
六、快速排序
1.原理
任取待排序元素序列中的某元素作为基准值(一般去末尾元素),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.代码实现
代码如下(示例):
快排的基本框架
void QuickSort(int * arr, int left, int right)
{
//递归到一定程度 元素较少时 用插入排序
if (left >= right)
{
return;
}
//int mid = Partion1(arr, left, right);
int mid = Partion1(arr, left, right);
//左子序列排序
QuickSort(arr, left, mid-1);
//右子序列排序
QuickSort(arr, mid+1, right);
}
基准值的选取
```c
//快排key值的选取
//三数取中法:找到最终间数据的位置
int GetMiddleIndex(int * arr, int left, int right)
{
int mid = left + ((right - left) >> 1);
if (arr[left] < arr[right])
{
if (arr[mid] < left)
{
return left;
}
else if (arr[mid] > arr[right])
{
return right;
}
else
return mid;
}
else //arr[left] >= arr[right]
{
if (arr[mid] > arr[left])
{
return left;
}
else if (arr[mid < arr[right]])
{
return right;
}
else
return mid;
}
}
我们选取基准值为末尾元素,对左右子序列的划分
hoare版本
//hoare版本
int Partion1(int * arr, int left, int right)
{
int mid = GetMiddleIndex(arr, left, right);
swap(&arr[mid], &arr[right]);
int tmp = right;
int key = arr[right];
while (left < right)
{
//left < right 保证合法范围内
//找到>=key的值
while (left < right && arr[left] < key)
{
left++;
}
//找到<key的值
while (left < right && arr[right] >= key)
{
right--;
}
swap(&arr[left], &arr[right]);
}
if (left != tmp)
{
swap(&arr[left], &arr[tmp]);
}
return left;
}
挖坑法
//挖坑法
int Partion2(int * arr, int left, int right)
{
int mid = GetMiddleIndex(arr, left, right);
swap(&arr[mid], &arr[right]);
int key = arr[right];//形成坑位
while (left < right)
{
//找到>=key的值
while (left <= right && arr[left] < key)
{
left++;
}
//必须有if判断 保证left和right在同一位置
if (left != right)
{
arr[right] = arr[left];//填坑后left处变成坑位
right--;
}
//找到<key的值
while (left < right && arr[right] >= key)
{
right--;
}
if (left != right)
{
arr[left] = arr[right];//填坑后right变成坑位
left++;
}
}
arr[left] = key;
return left;
}
前后指针法
//前后指针法
int Partion3(int * arr, int left, int right)
{
int mid = GetMiddleIndex(arr, left, right);
swap(&arr[mid], &arr[right]);
int cur = left;
int prev = cur-1;
int key = arr[right];
while (cur <= right)
{
//++prev始终指向的是>=key的第一个元素
if (arr[cur] < key && ++prev != cur)
{
swap(&arr[prev], &arr[cur]);
}
++cur;
}
if (++prev != right)
{
swap(&arr[prev], &arr[right]);
}
return prev;
}
快速排序的优化:
问题:
- 当元素个数比较多时,递归调用深度太深导致程序奔溃
- key值选取的不同,是快速排序效率主要因素
优化:
- 递归到一定程度时,left到right区间的元素比较少(达到一定阈值后),我们可以使用插入排序来减少递归调用的次数
- 我们可以使用上述的三数取中法来选取key值
快速排序的非递归
void QuickSortNonR(int * a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (!StackEmpty(&st))
{
right = StackTop(&st);
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
if(left >= right)
{
continue;
}
//划分
int div = Partion1(a, left, right);
//[div+1, right]
StackPush(&st, div+1);
StackPush(&st, right);
//[left, div]
StackPush(&st, left);
StackPush(&st, div);
}
StackDestroy(&st);
}
3.时间复杂度
快排的时间复杂度取决于基准值key的选取
假设基准值选取的比较好,每次基准值划分的左右子序列列数目相等,
如果基准值选取的不好
所以上述我们采用了三数取中法来选取基准值
4. 空间复杂度
递归调用的深度*单次使用的空间
O(logN)
5.稳定性
不稳定
6.应用场景
快速排序主要应用在元素顺序比较混乱的情况下
七、归并排序
1.原理
采用分治法,先让子序列有序,再合并子序列.
2.代码实现
代码如下(示例):
递归:
//合并数据
void MergeData(int *arr, int * temp, int left, int mid, int right)
{
int index = left;
int begin1 = left, end1 = mid;
int begin2 = mid, end2 = right;
while (begin1 < end1 && begin2 < end2)
{
if (arr[begin1] <= arr[begin2])
{
temp[index++] = arr[begin1++];
}
else
{
temp[index++] = arr[begin2++];
}
}
while (begin1 < end1)
{
temp[index++] = arr[begin1++];
}
while (begin2 < end2)
{
temp[index++] = arr[begin2++];
}
}
void _MergeSort(int * arr, int * temp, int left, int right)
{
if (right - left > 1) // [left, right)
{
//1.对区间进行均分
int mid = left + ((right - left) >> 1);
//2.[left, mid) 和 [mid, right)
_MergeSort(arr, temp, left, mid);
_MergeSort(arr, temp, mid, right);
//3.归并
MergeData(arr, temp, left, mid, right);
//4.拷贝回原区间 [ )
memcpy(arr+left, temp+left, (right-left)*sizeof(arr[0]));
}
}
//左闭右开区间
void MergeSort(int * arr, int len)
{
int * temp = (int *)malloc(sizeof(arr[0]) * len);
if (NULL == temp)
{
assert(0);
return;
}
_MergeSort(arr, temp, 0, len);
free(temp);
}
非递归:
void MergeSortNor(int * arr, int len)
{
//printf("归并排序的非递归\n");
int * temp = (int*)malloc(sizeof(arr[0]) * len);
if (!temp)
{
assert(0);
return;
}
int gap = 1;
int i = 0;
while (gap < len)
{
for (i; i < len; i+=2*gap)
{
//[ )
int left = i;
int mid = left + gap;
int right = mid + gap;
if (mid > len)//mid可能越界
mid = len;
if (right > len)//right可能越界
right = len;
MergeData(arr, temp, left, mid, right);
}
memcpy(arr, temp, len*sizeof(arr[0]));
gap *=2;
}
free(temp);
}
3.时间复杂度
O(NlogN)
4. 空间复杂度
O(N)+O(logN),即O(N)
5.稳定性
稳定
6.应用场景
数据量大,无法一次性加载到内存的情况
七、计数排序
1.原理
- 求出区间的范围
- 根据范围,申请计数空间
- 在计数空间中统计每个元素的出现个数
- 根据计数空间,对数据进行回收
2.代码实现
代码如下(示例):
void CountingSort(int * arr, int len)
{
//计算区间范围
int minValue = arr[0];
int maxValue = arr[0];
int i = 1;
for (i; i < len; i++)
{
if (arr[i] < minValue)
{
minValue = arr[i];
}
if (arr[i] > maxValue)
{
maxValue = arr[i];
}
}
//PrintArr(arr, len);
//计算空间大小
int size = maxValue - minValue + 1;
//申请计数空间,并初始化为0
int * temp = (int*)calloc(size, sizeof(int));
//统计每个元素的个数
for (i = 0; i < len; i++)
{
temp[arr[i]-minValue]++;
}
//对数据进行回收
int index = 0;
for (i = 0; i < size; i++)
{
if (temp[i])
{
while (temp[i]--)
{
arr[index++] = i+minValue;
}
}
}
//PrintArr(arr, len);
free(temp);
}
3.时间复杂度
O(N+K)
4. 空间复杂度
O(K)
5.稳定性
稳定
6.应用场景
数据主要集中在某个范围
八、桶排序
1.原理
对每个元素进行分桶
桶内有序(插入排序)
对每个桶进行合并(链表合并)
2.代码实现
代码如下(示例):
//桶的定义
const int BUCKET_NUM = 10;
struct ListNode{
ListNode(int i = 0):mData(i),mNext(NULL){}
ListNode * mNext;
int mData;
};
//桶排序
//插入结点
ListNode * insert(ListNode * head, int val)
{
ListNode dummyNode;//使用虚拟头节点
ListNode * newNode = new ListNode(val);
dummyNode.mNext = head;
ListNode * pre = &dummyNode;
ListNode * cur = head;
//找到第一个大于val的结点
while (cur && cur->mData <= val)
{
pre = cur;
cur = cur->mNext;
}
//插入
newNode->mNext = cur;
pre->mNext = newNode;
return dummyNode.mNext;
}
//链表合并
ListNode * Merge(ListNode * head1, ListNode * head2)
{
ListNode dummyNode;
ListNode * dummy = &dummyNode;
while (head1 && head2)
{
if (head1->mData <= head2->mData)
{
dummy->mNext = head1;
head1 = head1->mNext;
}
else
{
dummy->mNext = head2;
head2 = head2->mNext;
}
dummy = dummy->mNext;
}
if (head1)
{
dummy->mNext = head1;
}
if (head2)
{
dummy->mNext = head2;
}
return dummyNode.mNext;
}
void BucketSort(int * arr, int n)
{
vector<ListNode*> bucket(BUCKET_NUM, (ListNode*)(0));
int i = 0;
for (; i < n; i++)
{
int index = arr[i] / BUCKET_NUM;//分桶
ListNode * head = bucket.at(index);
bucket.at(index) = insert(head, arr[i]);//插入
}
ListNode * head = bucket.at(0);
for (i = 1; i < BUCKET_NUM; i++)
{
head = Merge(head, bucket.at(i));//链表合并
}
#if 0
for (i = 0;i < BUCKET_NUM; i++)
{
ListNode * head = bucket.at(i);
while (head)
{
printf("%d ", head->mData);
head = head->mNext;
}
}
#endif
for (i = 0;i < n; i++)
{
arr[i] = head->mData;
head = head->mNext;
}
}
3.时间复杂度
关键在于把元素均匀的分配到每个桶里
O(N+K)
4. 空间复杂度
O(K)
5.稳定性
稳定
6.应用场景
桶排序比较适合用做外部排序,数据量很大的适合,内存空间不足,就可以一个桶一个桶的排序,排完之后在按桶的顺序输出就行
九、基数排序
1.原理
2.代码实现
代码如下(示例):
//基数排序
void RadixSort(int * arr, int n)
{
int d = maxBit(arr, n);//位数
int * tmp = new int[n];
int * count = new int[10];
int i,j,k;
int radix = 1;
for (i = 1; i < d; i++)
{
//每次分配前清空计数器
memset(arr, 0, sizeof(arr[0] * n));
for (j = 0; j < n; j++)
{
k = (arr[j] / radix) % 10;
count[k]++;
}
//累计元素
for (j = 1; j < 10; j++)
{
count[j] = count[j-1] + count[j];
}
//通过倒叙实现稳定
for (j = n-1; j >= 0; j--) //将所有桶中记录一次收集到tmp中
{
k = (arr[j] / radix) % 10;
tmp[count[k] - 1] = arr[j];
}
//拷贝回arr中
memcpy(arr, tmp, n*sizeof(arr[0]));
radix *= 10;
}
delete []tmp;
delete[]count;
}
3.时间复杂度
O(NK)*
4. 空间复杂度
O(N+K)
5.稳定性
稳定
6.应用场景
*当数据量很大,数值也很高,但是数据量之间存在前后关系,例如手机号码