目录
一、排序算法的基本概念
算法的稳定性。若待排序表中有两个元素Ri和Rj,其对应的关键字相同即keyi= keyj,且在排序前Ri在Rj的前面,若使用某一排序算法排序后,Ri仍然在Rj的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的。
1.1 排序算法的分类
1.1.1 内部排序
数据都在内存中
1.1.2 外部排序
数据太多,无法全部存入内存
二、插入排序
//插入排序算法
void insertsort(int a[], int n)
{
int i, j, temp;
for (i = 1; i < n; i++)
{
if (a[i] < a[i - 1])
{
temp = a[i];
for (j = i - 1; j >= 0 && a[j] > temp; j--)//j跳出循环的时候为-1
{
a[j + 1] = a[j];
}
a[j + 1] = temp;
}
}
}
这里可以采用(带哨兵)的优化算法,代码如下:
//直接插入排序 (带哨兵)
void InsertSort(int A[], int n)
{
int i, j;
for (i = 2; i <= n; i++)//依次将A[2]~A[n]插入到前面已排序序列
{
if (A[i] < A[i - 1]) //若A[i]关键码小于其前驱,将A[i]插入有序表
{
A[0] = A[i]; //复制为哨兵,A[0]不存放元素
for (j = i - 1; A[0] < A[j]; --j) //从后往前查找待插入位置
A[j + 1] = A[i];//向后挪位
A[j + 1] = A[0]; //复制到插入位置
}
}
}
在已经有序的前提下,进行插入操作,还可以优化为折半插入排序,在使用折半插入排序时,当 low>high 时折半查找停止,应将 [ low , i-1] 内的元素全部右移,并将A[0]复制到 low 所指位置当 A[mid]==A[0] 时,为了保证算法的“稳定性”,应继续在 mid 所指位置右边寻找插入位置。
三、希尔排序
希尔排序: 先将待排序表分割成若干形如 L[ i , i+d , i+ 2d, ... , i+ kd ] 的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到 d =1为止。(步长 d 每次折半)
希尔排序代码如下:
//希尔排序
void shellsort(int a[], int n)
{
int i, j, d;
for (d = n / 2; d >= 1; d = d / 2)//步长变化
{
for (i = 1 + d; i < n; i++)
{
if (a[i] < a[i - d])//需要将a[i]插入有序增量子表
{
a[0] = a[i];//暂存在a[0]
for (j = i - d; j > 0 && a[j] > a[0]; j = j - d)
{
a[j + d] = a[j];//记录后移,查找插入的位置
}
a[j + d] = a[0];//插入
}
}
}
}
注意:希尔排序不稳定,仅适用于顺序表,不适合链表。
四、冒泡排序
算法如下:
void bubblesort(int a[], int n)
{
for (int i = 0; i < n-1; i++)
{
int flag = 1;
for (int j = 0; j <n-i-1; j++)
{
if (a[j] > a[j + 1])
{
int temp = 0;
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
flag = 0;
}
}
if (flag == 1)
{
break;
}
}
}
其中 flag 作为算法是否结束的标志,当某一趟比较不需要交换时,说明已经有序,可以直接跳出循环。
五、快速排序
算法思想: 在待排序表 L[1...n] 中任取一个元素 pivot 作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分 L[1...k-1] 和 L[k+1...n],使得L[1...k-1]中的所有元素小于pivot,L[k+1...n]中的所有元素大于等于 pivot,则 pivot 放在了其最终位置 L(k) 上,这个过程称为一次“划分”。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
算法代码如下:
//确定枢轴元素在数组中的下标位置
//第一次执行时,用第一个元素将待排序序列划分成左右两个部分
int partition(int a[], int low, int high)
{
int pivot = a[low];
while (low < high)
{
while (low < high && a[high] < pivot)
--high;
while (low < high && a[low] > pivot)
++low;
}
a[low] = pivot;
return pivot;
}
//快速排序算法
void quicksort(int a[], int low, int high)
{
if (low < high)//跳出递归的条件
{
int pivotpos = partition(a, low, high);//划分
quicksort(a, low, pivotpos - 1);//划分左子表
quicksort(a, pivotpos + 1, high);//划分右子表
}
}
快速排序是不稳定的,算法表现主要取决于递归深度“划分”越不均匀,递归深度越深若每次“划分”越均匀,则递归深度越低 。
六、简单选择排序
选择排序: 每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列
算法代码如下:
//简单选择排序
void selectsort(int a[], int n)
{
for (int i = 0; i < n-1; i++)
{
int min = i;
for (int j = i + 1; j < n; j++)
{
if (a[j] < a[min])
{
min = j;
}
}
if (min != i)
{
int temp;
temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
该算法不稳定,适用性: 既可以用于顺序表,也可用链表 。
七、堆排序
判断 i 是否是叶子结点: i>n/2 ?
7.1 建立大根堆
思路: 1、把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
2、检查当前结点是否满足根>左、右。若不满足,将当前结点与更大的一个孩子互换
3、若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整 (小元素不断“下坠”)
建立大根堆代码如下:
//数组从下标1开始存数据
//建立大根堆
void buildmaxheap(int a[], int len)
{
for (int i = len / 2; i > 0; i--)//从后往前调整所有非终端结点
headadjust(a, i, len);
}
//将以k为根的子树调整为大根堆
void headadjust(int a[], int k, int len)
{
a[0] = a[k];//a[0]暂存子树结点
for (int i = 2*k; i < len; i++)//沿key较大的子节点向下筛选
{
if (a[i] < a[i + 1] && i < len)
i++;
if (a[i] <= a[0])
break;
else
{
a[k] = a[i];//将a[i]调整到双亲结点上
k = i;//修改k值,以便继续向下筛查
}
}
a[k] = a[0];//被筛结点值放入最终位置
}
7.2 基于大根堆进行排序
选择排序: 每一趟在待排序元素中选取关键字最大的元素加入有序子序列
堆排序: 每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换)
并将待排序元素序列再次调整为大根堆(小元素不断“下坠”)
//堆排序完整逻辑
void heapsort(int a[], int len)
{
buildmaxheap(a, len);
for (int i = len; i > 1; i--)
{
swap(a[i], a[1]);
headadjust(a, 1, i - 1);
}
}
结论: 一个结点,每“下坠”一层,最多只需对比关键字2次
若树高为 h,某结点在第 i 层,则将这个结点向下调整最多只需要“下坠” h-i 层,关键字对比次数不超过 2(h-i)
n 个结点的完全二叉树树高
该算法的时间复杂度为O(n),算法是不稳定的。
7.3 基于小根堆算法排序
下面为以小根堆进行排序,算法如下。(小根堆排序得到递减序列)
//将以k为根的子树调整为小根堆
void headadjust(int a[], int k, int len)
{
a[0] = a[k];//a[0]暂存子树结点
for (int i = 2 * k; i < len; i=i*2)//沿key较大的子节点向下筛选
{
if (a[i] > a[i + 1] && i < len)
i++;
if (a[i] >= a[0])
break;
else
{
a[k] = a[i];//将a[i]调整到双亲结点上
k = i;//修改k值,以便继续向下筛查
}
}
a[k] = a[0];//被筛结点值放入最终位置
}
//建立小根堆
void buildminheap(int a[], int len)
{
for (int i = len / 2; i > 0; i--)//从后往前调整所有非终端结点
headadjust(a, i, len);
}
//堆排序完整逻辑
void heapsort(int a[], int len)
{
buildminheap(a, len);
for (int i = len; i >1; i--)
{
int temp = 0;
temp = a[i];
a[i] = a[1];
a[1] = temp;
headadjust(a, 1, i -1);
}
}
7.4 堆的插入与删除
7.4.1 小根堆的插入
对于小根堆,新元素放到表尾,与父节点对比若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止。
7.4.2 小根堆的删除
被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。
八、归并排序
归并:把两个或多个已经有序的序列合并成一个。
归并排序: 在内部排序一般采用二路归并
核心操作: 把数组内的两个有序序列归并为一个
算法代码如下:
事先准备一个辅助数组B:int *B = (int*)malloc(n * sizeof(int));//辅助数组B
//a[low...mid]和a[mid+1...high]各自有序,将两个部分归并
void merge(int a[], int low, int mid, int high)
{
int i, j, k;
for (int k = low; k <= high; k++)
{
B[k] = a[k];//将A中所有元素复制到B中
}
for (int i = low, j = mid + 1, k = low; i <= mid, j <= high; k++)
{
if (B[i] > B[j])
{
a[k] = B[j++];//将较小值复制到A中
}
else
a[k] = B[i++];
}
while (i <= mid)
a[k++] = B[i++];
while (j<=high)
a[k++] = B[j++];
}
void mergesort(int a[], int low, int high)
{
if (low < high)
{
int mid = (low + high) / 2;
mergesort(a,low, mid);//对左半部分归并排序
mergesort(a, mid+1, high); //对右半部分归并排序
merge(a, low, mid, high);//归并
}
}
九、基数排序
基数排序,时间复杂度 = O(d(n+r))
基数排序擅长解决的问题
1、数据元素的关键字可以方便地拆分为 d 组,且d较小
2、每组关键字的取值范围不大,即r较小
3、数据元素个数 n 较大
十、外部排序
重要结论: 采用多路归并可以减少归并趟数,从而减少磁盘I/O(读写)次数
对r 个初始归并段,做k路归并,则归并树可用 k 叉树表示。若树高为h,则归并趟数 = h-1 =
推导: k叉树第 h 层最多有个结点则 r <= ,(h-1)最小 = 。
十一、败者树
k路归并的败者树只需要定义一个长度为 k 的数组即可。(叶子结点为虚拟的)
败者树解决的问题: 使用多路平衡归并可减少归并趟数,但是用老土方法从 k 个归并段选出一个最小/最大元素需要对比关键字 k-1次,构造败者树可以使关键字对比次数减少到
败者树可视为一棵完全二叉树(多了一个头头)。k个叶结点分别对应 k 个归并段中当前参加比较的元素,非叶子结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。
总结
可以登陆下面这个网站,查看各种算法的原理和演示过程。