//解决问题步骤的描述。C语言。
排序
//重新排序列中的元素,使其满足按关键字有序。
插入排序
- 将待排元素插入到有序序列中。
直接插入排序
(慢):适用于基本有序且数据量不大的排序表。
- 基本思想:【有序序列】【待插元素】【无序序列】
- 简析:从后往前依次比较有序序列元素,寻找待插元素 a[ i ] 的插入位置,如果大于待插元素,就将其后移,若小于则确定插入位置。初始把第一个元素,作为有序序列,之后剩余1~n元素为待插序列。
void InsertSort(int* arr,int n)
{
for(int i=1; i<n; ++i) //外侧循环表待插元素arr[i],需循环执行n-1次
{
int val = arr[i]; //保存待插入元素arr[i],避免覆盖
for(int j=i-1; j>=0 && val < arr[j]; --j) //每次循环将arr[i],由后往前依次与有序序列比较,交换,按升序寻找插入位置
{
a[j+1] = a[j];
}
a[j+1] = val; //插入元素,j=-1时也成立
}
}
折半插入排序
- 先折半查找,确定插入位置,统一将元素后移。
希尔排序
- 缩小增量排序,对直接插入排序的优化。
void ShellSort(int* arr,int n)
{
for(int gap=n>>1; gap>0; gap>>=1) //规定步长,直到gap=1
{
for(int i=gap; i<n; ++i) //for循环同直接插入排序,将1替换成gap即可
{
int val = arr[i];
for(int j=i-gap; j>=0 && val < arr[j]; j-=gap)
{
a[j+gap] = a[j];
}
a[j+gap] = val;
}
}
}
交换排序
- 根据两个元素的比较结果交换在序列中的位置。
冒泡排序
(慢)
- 基本思想:【无序序列】【有序序列】
- 每一趟冒泡,在无序序列中从前往后依次比较交换两个相邻元素,两元素大小逆序则交换之。
- 每一趟冒泡可以确定一个最大值,最多经历N-1趟排序。如果一趟冒泡中未发生交换,则说明序列已有序。
void BubbleSort(int* arr,int n)
{
for (int i=0; i<n-1; ++i) //外侧循环,代表第i趟排序,最多需N-1趟排序
{
bool flag = false; //判断本趟冒泡时,序列是否已经有序
for (int j=0; j<n-i-1; ++j) //每一趟排序,j遍历序列,由后往前确定一个元素最终位置
{
if (arr[j] > arr[j+1]) //将较大的后移
{
Swap(arr[j], arr[j+1]);
flag = true;
}
}
if(flag == false)
return;
}
}
快速排序
(快):无论是面试还是考研,务必掌握。
- 基本思想:分而治之思想。
- Partition划分函数:选取最后一个元素作为哨兵,将表一分为二,比哨兵小的放左边,比哨兵大的放右边,由此确定一个元素的最终位置,返回哨兵元素的下标。
- 根据哨兵位置,划分前后两个子序列,递归处理前后两个子序列,直到序列元素只有1或0个。
----> 注意:对于n个重复的元素序列,哨兵始终在最后,退化成选择排序,有n层递归,时间复杂度为O(n2),且容易出现栈溢出。递归在于每次调用的哨兵都需要额外存储。
设计分割算法:函数返回分割界限。
int Partition(int* a; int low; int high) //划分
{
int pivot = low; //表的第一个元素作为枢纽
for(int i=low; i<high; ++i) //for循环,i遍历数组,与哨兵high比较,非i<=high
{
if(a[i]<a[high]) //小于哨兵的元素前移,a[i]>=a[high]不影响
{
swap(a[pivot],a[i]); //i=pivot时没有问题
++pivot; //保证下标pivot之前的元素,都小于哨兵
}
}
swap(a[high],a[pivot]);
return pivot;
}
void QuickSort(int* arr; int low; int high)
{
if(low<high) //数组内至少有两个元素,否则递归终止
{
int pivotpos = Partition(arr,low,high); //划分数组,获取哨兵位置
QuickSort(arr,low,pivotpos-1); //递归处理前半部分,不再包含pivot
QuickSort(arr,pivotpos+1,high); //递归处理后半部分,不再包含pivot
}
}
选择排序
- 在无序序列中选择最小的元素,放在有序序列中,只需一次交换。
简单选择排序
(慢)
- 基本思想:【有序序列】【标记位置】【无序序列】
- 必须经历N-1趟排序,每一趟排序,在无序序列中选择最小元素,放在标记位置。
- 从i=0开始,初始a[min]=a[0],记录下标位置,与之后无序待选元素依次比较,记录最小的元素位置,最后与下标元素进行交换,一轮排序只需交换一次。
void SelectSort(int* arr,int n)
{
for(int i=0; i<n-1; ++i) //外侧循环,代表第i趟排序,也代表标记的交换位置,有N-1趟排序
{
int min = i;
for(int j=i+1; j<n; ++j) //无序序列中选择最小元素
{
if(arr[i]>arr[j]) min = j;
}
if(min!=i) swap(a[i],a[min]); //与标记位交换
}
}
堆排序
(性能好,思路好):重点当中的重点。完全二叉树(形状特征)----> 数组。结点编号:1 ~ N,数组下标:0 ~ N-1。
- 主要问题:如何建立初始堆?输出堆顶元素后,如何调整为新的堆?
1.对乱序初始N个元素建堆:从最后一个父结点开始(对应数组下标n/2-1~0),向下而上进行堆调整,使之成为大根堆。
2.交换堆顶与末尾元素并重建堆:堆的规模减一,只需从堆顶再调整一次堆结构即可,因为其余父结点的子树依旧稳定,循环执行到堆的大小为2。
----> 不稳定。时间复杂度:总共经历调用了(n/2+n)次调整函数,每次调整的最大深度为(log2n),所以最坏/平均/最好的时间复杂度:nlog2n,空间复杂度:O(1)。
//向下调整以pos数组下标为根结点的子树,len-1是最后一个堆元素的数组下标
void AdjustMaxHeap(int *a, int pos, int len) //一次根结点为pos的向下堆调整
{
int dad = pos; //dad数组下标是pos,dad的树编号是pos+1
int son =2*dad+1;//儿子的树编号是2*(pos+1),儿子的数组下标是2*pos+1
while(son<len) //儿子要在堆的范围内
{
if(son+1<len && a[son]<a[son+1]); //左右孩子比较
{
++son; //当兄弟存在且更大时,兄弟会作为父亲的挑战者
}
if(a[dad]<a[son])
{
swap(a[dad],a[son]);
dad = son; //被交换下来以后,开始向下循环比较
son = 2*dad+1; //循环把自己作为起点,向下查看堆的稳定性
}
else
{
break; //当父亲比儿子大,说明堆整体稳定
}
}
}
void HeapSort(int *a,int n)
{
for(int i=n/2-1; i>=0; --i ) //初始建大根堆,从最后一个父结点开始,自下而上调整子树,i是每次调整对应根结点的下标
+++++++++++++++++++++++................................................................................................................. {
AdjustMaxHeap(a,i,n);
}
swap(a[0],a[n-1]); //交换堆顶和最后一个元素,这样就放好了最大的元素,循环缩简堆的规模,重建堆,交换堆顶和最后一个元素
for(int i=n-1; i>1; --i) //这里的i是堆的规模,需再调整交换n-2次
{
AdjustMaxHeap(a,0,i); //只需一次向下调整,只有堆顶元素不满足
swap(a[0],a[i-1]);
}
}
归并排序
//递归。空间复杂度较大。
- 分而治之的思想:划分左右子序列,递归排序。辅助数组 b,先存放原有两个数组的数据,逐项比较插回原有数组,实现一次二路归并。
----> 特点:归并排序可以做多机排序,外排序(磁盘文件之类),对于处理海量数据。
void merge(int *a, int left, int mid, int right) //合并一次子序列
{
int *b = (int*)malloc(N*sizeof(int)); //多次malloc费时,可以放在外边定义
for(int k=left; k<=right; ++k)
{
b[k] = a[k]; //将两个子序列数组,拷贝到b里面
}
int i,j,k; //i访问b左边的序列,j访问b右边的序列,k访问目标原序列
for(i=left, j=mid+1, k=left; i<=mid && j<=right; ++k)
{
if(b[i]<b[j])
{
a[k] = b[i]; //将比较小的元素放回
++i; //i往后移
}
else
{
a[k] = b[j];
++j;
}
}
while(i<=mid) //如果辅助序列中b中,还有剩余元素,继续放回
{
a[k] = b[i];
++k;
++i;
}
while(j<=right)
{
a[k] = b[j];
++k;
++j;
}
free(b);
b = NULL;
}
void MergeSort(int *a. int left, int right) //划分递归排序
{
if(left<right)
{
int mid = (left+right)/2; //从中间划分两个子序列
MergeSort(a,left,mid); //中间点在递归中会被考虑进去(与快排不同)
MergeSort(a,mid+1,right);
Merge(a,left,mid,right); //合并两个有序子序列
}
}
分配类
//基数排序,计数排序,桶排序
基数排序
计数排序
(Counting Sort)
时间复杂度,O(N+M(辅助数组))。空间换时间。需要一个辅助数组,统计存储待排序数据的数值范围。遍历数据根据其重复次数,依次填入原有数组,最终有序。
适用:
- 对时间要求很敏感。
- 数据规模可控。数据跨度不宜过大,浪费空间。必须是正整数,是以数组下标代表数值。
- 考虑重复的时候。
void CountSort(int *a, int n)
{
int count[MAX] = {0}; //max为最大最小数据之差
for(int i=0; i<n; ++i)
{
++count[a[i]]; //把元素的数值作为辅助数组的下标,访问并修改该元素重复出现的次数
}
for(int i=0,k=0; i<MAX; ++i)
{
for(int j=0; j<count[i]; ++j)//把数据根据重复次数写回原数组
{
a[k] = i;
++k;
}
}
}
查找
顺序查找
n
二分查找
//又称折半查找。仅适用于有序的顺序表。
-
基本思想:将待查找元素和中间位置的元素进行大小比较,根据比较结果缩小比较规模。
----> 时间复杂度:O(log2n)。
哈希表
(散列表)
//如果使用类似计数排序的方式,按辅助数组下标对应数据的关键字,来存储其地址,一是空间浪费,二是只能存储正整数的关键字数据。
使用哈希的流程:
-
插入。将元素传入hash函数,得到哈希值(非负整数),再将哈希值作为数组索引,将数据信息存入对应的数组中。
----> 设计哈希函数:依照数据的关键字映射函数,确定在哈希表中的位置。使用别人设计好的哈希函数!!!
-----> 设计冲突解决办法:开放地址法(重新寻找位置),链表法(通常使用这种,待查找信息封装成结点,hashTable存放的是链表的头指针)。不同的元素通过hash函数得到一样的结果。
-
查找。将元素传入函数得到哈希值,再将哈希值作为数组索引,在哈希表中找到对应信息。
//王道教材
#define MAXKEY 1000
int hash(char *key) //哈希范围,0 ~ MAXKEY-1
{
int h = 0,g;
while (*key)
{
h = (h<<4) + *key++;
g = h & 0xf000000;
if(g)
h ^= g>>24;
h &= ~g;
}
return h % MAXKEY;
}
//main函数
int main()
{
char *hashTable[MAXKEY] = {0};
char *str[] = {
"MARY","LINDA","DAVID","THOMAS","ROBERT","SUSAN"
}
for(int i=0; i<sizeof(str)/sizeof(char*); ++i)
{
//根据hash值将数据插入到哈希表hahTable中
hashTable[hash(str[i])] = str[i]; //存放的是地址
}
char in[20] = {0};
while(scanf("%s",in) != EOF)
{
//根据输入数据的hash值,查找哈希表
if(hashTable[hash(in)] == NULL)
{
printf("NO!\n");
}
else
{
printf("YES!\n");
}
}
}
BST树
(二叉查找树 / 二叉搜索树/二叉排序树)
//二叉检索树也是我们最熟悉的一个索引方式了。可以通过大小比较关系来进行快速的检索,在一棵满二叉平衡树的情况下,检索的效率可以达到logn(类似二分检索),然后插入和删除的效率也是稳定的logn。
性质:左子树最大值 < 任意结点 < 右子树最大值。中序遍历是递增序列。
----> 同样的元素可以构成不同的二叉查找树。
缺点:由于BST没有相关措施保持平衡,很容易因为插入数据的不对导致树的不平衡,当极度不平衡的时候,BST就会退化成一个链表查询,搜索的速度自然也就降低到了N,相关维护的代价也高(要想插入和删除,前提是要找到该节点)。
BST的插入:结点与根比较,如果大选右子树,如果小选左子树,作为下一次递归的起点,直到遇到空位置插入。
BST的删除:度为0的叶子结点,直接删除。度为1的结点,只有一个孩子,用结点的孩子去取代它的位置。度为2的结点,找到其左子树的最大值或右子树的最小值(它的度必然是0或1),拷贝这个值覆盖待删除结点,再删除掉度为0或1的结点。
AVL树
(平衡二叉搜索树:Self-balancing binary search tree)
//AVL树解决了BST中容易出现不平衡的问题,任一 节点对应的两棵子树的最大高度差为1。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。
----> 旋转:保持二叉树性质和数据内容不变,改变BST的结构。取出两个父子结点。左旋,父–>左孩子。右旋,父–>右孩子。
性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(log2n)。
缺点:维护的代价过高,相较于BST,AVL树要有一个保持平衡的旋转操作,并且要保持严格平衡,所以维护代码高。
红黑树
(RB树:Red-Black tree)
//自平衡二叉查找树。可在O(log2n)时间内完成,查找,插入和删除。
//红黑树相对于AVL树的时间复杂度是一样的,但是优势是当插入或者删除节点时,因为可染色,红黑树实际的调整次数更少,旋转次数更少,因此红黑树插入删除的效率高于 AVL树,大量中间件产品中使用了红黑树。用途非常广泛:凡是需要树形结构存储数据的地方,除了文件和磁盘。是相对平衡的二叉排序树中,相对简单的一种形式??原理思路代码简单。
----> 红黑树相对于AVL树来说,牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能效率要优于AVL树。
性质特征:
-
根必须是黑色。
-
结点必须是红色或者黑色。
-
所有叶子结点(值为NULL)都是黑色。
-
每个红色结点必须有两个黑色孩子,红红不相邻。
-
任一结点出发到其每个叶子结点的所有简单路径都包含相同数目的黑色结点,黑色路径相同。
----> 由4,5推导,红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,自平衡。
红黑树的插入:插入的结点是红色的。插入位置父是红,看叔叔。
- 情况1(插入位置在根结点,空树?):只需将红色的根结点染黑。
- 情况2(父结点是黑色):不需要任何改变。
- 情况3(父结点是红色,叔叔结点是红色):将父亲和叔叔染黑,将爷爷染红(保证简单路径上黑色数量不变)。此时可能爷爷是根结点,或者祖父节点是红。故将爷爷作为新插入的结点,再进行调整。
- 情况4(父结点是红色,叔叔结点是黑色或者不存在,孩子大小在爸爸和爷爷之间,”腰“型):旋转父亲和孩子,使其转换成”裙子“型,通过情况5进一步调整。
- 情况5(父结点是红色,叔叔结点是黑色或者不存在,孩子大小小于或者大于爸爸和爷爷,”裙子“型):旋转父亲和爷爷,将父亲染黑,父亲的左右孩子是红色。
红黑树的删除: