目录
上标x2,下标x0
0.前言
-
排序四象限原则:稳定/非稳定、内部/外部
稳定排序:值相同的元素在排序前后相对位置不变
内部排序:整体数据加载到内存中进行排序 -
本节内容排序均为递增
1. 稳定排序
1.1 插入排序(内部)
- 时间复杂度o(n2),基于比较和交换
1.2 冒泡排序(内部)
- 时间复杂度o(n2),基于比较和交换
- 优化
某一轮冒泡过程没有任何交换操作的时候,说明数组有序,结束整个冒泡排序
1.3 归并排序(外部)
-
时间复杂度o(nlogn),基于比较
-
思考:2个有序数组合并为1个有序数组的方法?
定义3个指针指向3个数组,比较两个小数组的指针指向的值,插入到大数组中 -
分治思想:大规模化小规模,直到可以排序
-
2路归并
K路归并-堆
1.4 代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
//异或,交换两个变量的值
#define swap(a, b) {\
a ^= b; b ^= a; a ^= b;\
}
#define TEST(arr, n, func, args...) {\
int *num = (int *)malloc(sizeof(int) * n);\
memcpy(num, arr, sizeof(int) * n);\
printf("%s = ", #func);\
func(args);\
free(num);\
}
void insert_sort(int *num, int n) {
for (int i = 1; i < n; i++) {
for (int j = i; j > 0 && num[j] < num[j - 1]; j--) {
swap(num[j], num[j - 1]);
}
}
return ;
}
void bubble_sort(int *num, int n) {
// 优化,times表示进行交换的次数,times为0结束冒泡排序
int times = 1;
for (int i = 1; i < n && times; i++) {
times = 0; // 交换前times清空
for (int j = 0; j < n - i; j++) {
if (num[j] <= num[j + 1]) continue; // 对偶
swap(num[j], num[j + 1]);
times++; // 发生交换+1
}
}
return ;
}
void merge_sort(int *num, int l, int r) {
if (r - l <= 1) { // 元素个数<=2
if (r - l == 1 && num[r] < num[l]) {
swap(num[r], num[l]);
}
return ;//必须要有返回,不加这行会段错误
}
int mid = (l + r) >> 1;
merge_sort(num, l, mid);
merge_sort(num, mid + 1, r);
int *temp = (int *)malloc(sizeof(int) * (r - l + 1));
int p1 = l, p2 = mid + 1, k = 0;
while (p1 <= mid || p2 <= r) {
if (p2 > r || (p1 <= mid && num[p1] < num[p2])) {
temp[k++] = num[p1++];
} else {
temp[k++] = num[p2++];
}
}
memcpy(num + l, temp, sizeof(int) * (r - l + 1));
free(temp);
return ;
}
void randint(int *num, int n) {
while (n--) num[n] = rand() % 100;
return ;
}
void output(int *num, int n) {
printf("[");
for (int i = 0; i < n; i++) {
i && printf(" ");
printf("%d ", num[i]);
}
printf("]\n");
return ;
}
int main() {
srand(time(0));
#define max_n 20
int arr[max_n];
randint(arr, max_n);
//测试函数,实现一个TEST宏
TEST(arr, max_n, insert_sort, num, max_n);//数组,大小,排序,参数
TEST(arr, max_n, bubble_sort, num, max_n);
TEST(arr, max_n, merge_sort, num, 0, max_n - 1);
return 0;
}
2. 非稳定排序
2.1 选择排序
- 时间复杂度:o(n2),基于比较和交换
- 不稳定排序现象:53251,第一次选择排序时第一个5跑到最后面,相对位置改变
2.2 快速排序(先确定一个元素的位置)
- 快速排序,时间复杂度为O(nlogn),退化为O(n2)
- 基础的快速排序算法思想很简单,核心就是一句话:找到基准值的位置。
- 算法核心是partition方法,即把元素分开两组的方法,每次把元素平均分到两边时,算法效率最高。相反,如果每次partition把元素完全分到一边,逆序数组是最差情况,算法退化为O(n2)。
-
【理解】:
快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了
快速排序其实是冒泡排序的一种改进,冒泡排序每次对相邻的两个数进行比较,这显然是一种比较浪费时间的。 -
基数:一般选数组的第一个元素,作为基准值
-
partition操作:第一个位置的值从思维逻辑上拿出去,该处数据可以被覆盖掉,变为一个空的格子;尾头指针开始交替找小于基数的值、大于基数的值;头尾指针指向同一个位置,就是基准值应该存放的位置,把基准值存进去;对基准值两侧的数组进行partition操作
-
基本有序:一次partition操作把数组分为2部分,左面都小于基准值,右面都大于基准值
-
为什么不是稳定在o(nlogn)呢?逆序数组7654321,变成了选择排序,指针移动一步交换一次,时间复杂度o(n2)
2.3 代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
//此处交换值不能用异或的方法
//异或交换值有一个条件,两个元素不能异或自身,一旦异或就找不回来了
//__typeof获取相应变量的类型,定义一个额外的变量
#define swap(a, b) {\
__typeof(a) __temp = a;\
a = b; b = __temp;\
}
//变参列表用变参宏去实现
//\将多行宏连接成一行
//func(args),不是args...
#define TEST(arr, n, func, args...) {\
int *num = (int *)malloc(sizeof(int) * n);\
memcpy(num, arr, sizeof(int) * n);\
output(num, n);\
printf("%s = ", #func);\
func(args);\
output(num, n);\
free(num);\
}
void select_sort(int *num, int n) {
//从待排序区找一个最小值,跟待排序区的第一个元素去交换,然后成为已排序区的最后一个元素
//外层循环n-1轮
for (int i = 0; i < n - 1; i++) {
//待排序区的最小值的下标为ind,待排序区第1个的下标为i
int ind = i;
//内层循环,j去扫一遍待排序区,找一个最小值的下标
//j从待排序区第2个直到下标n-1
for (int j = i + 1; j < n; j++) {
if (num[ind] > num[j]) ind = j;
}
swap(num[i], num[ind]);
}
return ;
}
//快速排序,对num数组从l到r这个区间内进行排序
void quick_sort(int *num, int l, int r) {
if (l > r) return ;
//z为基准值,当前数组的第1个元素
int x = l, y = r, z = num[x];
//2个指针未重合,进行partition操作
//退出while时就是x==y了
while (x < y) {
//指针从后往前,退出while时x==y || num[y]<=z
while (x < y && num[y] > z) y--;
//x<y时,说明num[y]<=z了,找到了一个比基准值小的值,把它放到x指向的位置中
if (x < y) num[x++] = num[y];
//同理
while (x < y && num[x] < z) x++;
if (x < y) num[y--] = num[x];
}
num[x] = z;
//以上完成了一次partition操作,x==y
//对基准值两侧实现partition操作
quick_sort(num, l, x - 1);
quick_sort(num, x + 1, r);
return ;
}
void randint(int *num, int n) {
//注意该处,n初始值为n,赋值时为num[n - 1]
while (n--) num[n] = rand() % 100;
return ;
}
void output(int *num, int n) {
printf("[");
for (int i = 0; i < n; i++) {
printf("%d ", num[i]);
}
printf("]\n");
return ;
}
int main() {
srand(time(0));
#define max_n 20
int arr[max_n];
//给数组arr的每一位随机生成一个值
randint(arr, max_n);
//TEST宏进行测试
//依次传进来arr数组,大小,排序,相关的参数列表
TEST(arr, max_n, select_sort, num, max_n);
TEST(arr, max_n, quick_sort, num, 0, max_n - 1);
return 0;
}
2.3 快速排序的优化
【优化1】:基准值选中间值
//逆序情况下,基准值是第1位就不合理;基准值是任意的
【优化2】 :两侧递归,改为一部分用循环
【优化3】 :partition操作的优化:找到两个值互换
//头指针从前往后,尾指针从后往前,然后头尾指针的值做交换
void quick_sort(int *num, int l, int r) {
while (l < r) {
int x = l, y = r, z = num[(l + r) >> 1];//优化1
//x==y时也可以交换
do {
while (x <= y && num[x] < z) x++;
while (x <= y && num[y] > z) y--;
//执行完进行判断,x<=y说明左侧找到了大于的值,右侧找到了小于的值
if (x <= y) {
swap(num[x], num[y]);//优化3
x++, y--;
}
} while (x <= y);
//递归z的右侧,此时x为右侧第一个元素
quick_sort(num, x, r);
//条件l<r的r变为y,此时y是左侧最后一个元素,循环的是左侧的部分
r = y;//优化2
}
}
3. 查找
3.1 二分查找
-
二分查找与三分查找:分的是问题规模
-
应用条件:待查找序列单调
-
解决问题:x在单调序列中存不存在,存在返回下标,不存在返回特殊的值
-
数组的性质:下标映射到值
-
二分查找之所以用到数组,其实是需要一种映射关系,之前讲到传函数的二分查找
-
普通二分查找
-
二分查找特殊情况1
得到最后一个1的方式:min和max不断靠拢,直至整个待查找区间只剩一个元素,就找到了最后一个1
(错误示范)
特殊情况:全是0,最后头尾指针都指向0下标,有歧义:最后一个1在下标0处
(正确示范)
说明:数组0的前一位增加虚拟位,可以认为min = -1,最终判断min是否等于-1
- 二分查找特殊情况2
问题模型抽象:1代表满足某种性质与条件,求边界条件
3.2 三分查找
求凹凸函数极值点问题
3.3 代码
#include <stdio.h>
#define P(func) {\
printf("%s = %d\n", #func, func);\
}
//1 3 5 7 9 10...
int binary_search1(int *num, int n, int x) {
int head = 0, tail = n - 1, mid;
while (head <= tail) {
mid =(head + tail) >> 1;
if (num[mid] == x) return mid;
if (num[mid] < x) head = mid + 1;
else tail = mid - 1;
}
return -1;
}
//111111100000000
int binary_search2(int *num, int n) {
//head指向虚拟头
int head = -1, tail = n - 1, mid;
while (head < tail) {//待查找区间还剩至少1个元素
mid = (head + tail + 1) >> 1;//+1作用是为了避免出现死循环
if (num[mid] == 1) head = mid;
else tail = mid - 1;
}
return head;
}
//00000000000111111111
int binary_search3(int *num, int n) {
int head = 0, tail = n, mid;//虚拟尾指针
while (head < tail) {
mid = (head + tail) >> 1;
if (num[mid] == 1) tail = mid;
else head = mid + 1;
}
return head == n ? -1 : head;//三目运算符
}
int main() {
int arr1[10] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
int arr2[10] = {1, 1, 1, 1, 0, 0, 0, 0, 0, 0};
int arr3[10] = {0, 0, 0, 0, 0, 1, 1, 1, 1, 1};
P(binary_search1(arr1, 10, 7));
P(binary_search2(arr2, 10));
P(binary_search3(arr3, 10));
return 0;
}
3.4 哈希表
- 哈希表是一种数据结构,底层实现用到了数组,是把大的集合分组为小的集合,方便查找
前面讲的都是算法
- 举例:电话簿
- 实现哈希表我们可以采用两种方法:
1、数组+链表
2、数组+二叉树
数组:
- 数组是展开的函数,函数是压缩的数组,即数组和函数有联系
共同点是都包含一种映射关系 - 查找时间复杂度o(1),因为数组是顺序存储的,通过地址+偏移量可以直接访问
- 下标【int】映射为值【任意类型】,这是哈希表实现的灵感来源
哈希表:
- 值【任意类型】映射为下标【int】,把值存储在该下标位置
- 核心内容:哈希函数、冲突处理方法
- 冲突处理方法有4类:开放定址法,再哈希表、拉链法、建立公共溢出区
开放定址法,产生数据堆据问题
拉链法,在顺序表中存储的是链表
题目:
3.5 代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//哈希表==链表+顺序表
typedef struct Node {
char *str;
struct Node *next;
} Node;
typedef struct HashTable {
Node **data;//**
int size;
} HashTable;
//str插入head作为头节点的链表中
Node *init_node(char *str, Node *head) {
Node *p = (Node *)malloc(sizeof(Node));
p->str = strdup(str);//strdup()
//头插法
p->next = head;
return p;
}
HashTable *init_hashtable(int n) {
HashTable *h = (HashTable *)malloc(sizeof(HashTable));
h->size = n << 1;//哈希表扩大2倍存更多元素,50%利用率
h->data = (Node **)calloc(h->size, sizeof(Node *));//calloc
return h;
}
【注】:BKDRhash是一种字符串哈希算法,根据字符串生成hash值
int BKDRhash(char *str) {
//seed为质数
int seed = 31,hash = 0;
for (int i = 0; str[i]; i++) hash = hash * seed + str[i];
//hash超出int时会表示为负数,按位与0x7fffffff(符号位为0,其他位都是1),符号为变为0(正),其他位置不变
return hash & 0x7fffffff;
}
int insert(HashTable *h, char *str) {
//字符串映射为整型数组下标,用BKDRhash
int hash = BKDRhash(str);
int ind = hash % h->size;//%使下标合法
h->data[ind] = init_node(str, h->data[ind]);
return 1;
}
int search(HashTable *h, char *str) {
int hash = BKDRhash(str);
int ind = hash % h->size;
Node *p = h->data[ind];
while (p && strcmp(p->str, str)) p = p->next;
return p != NULL;
}
void clear_node(Node *node) {
if (node == NULL) return ;
Node *p = node, *q;
while (p) {
q = p->next;
free(p->str);
free(p);
p = q;
}
return ;
}
void clear_hashtable(HashTable *h) {
if (h == NULL) return ;
for (int i = 0; i < h->size; i++) {
clear_node(h->data[i]);
}
free(h->data);
free(h);
return ;
}
int main() {
int op;
#define max_n 100
char str[max_n + 5] = {0};
HashTable *h = init_hashtable(max_n + 5);
while (~scanf("%d%s", &op, str)) {
switch (op) {
case 0:
printf("insert %s to the HashTable\n", str);
insert(h, str);
break;
case 1:
printf("search %s from the HashTable result = %d\n", str, search(h, str));
break;
}
}
#undef max_n
clear_hashtable(h);
return 0;
}
【亮点】
- 针对不同数据,优秀的哈希函数和冲突处理方法不同,主要是哈希函数不同
对字符串查找时,哈希函数用BKDRhash,冲突处理方法采用拉链法 - 哈希表需要一片连续的存储空间存储任意类型,当前任意类型的每一位存的是一个链表
Node **data;
*data表示data指向连续存储空间的首地址,每一位是Node * - strdup()把传进来的字符串拷贝一份,返回拷贝后的字符串的新地址
是一种字符串拷贝库函数,一般和free函数成对出现 - 哈希表的利用率50%-90%,工业上达到70%即可,哈希表利用率和冲突函数有关,只要是哈希函数就一定会有冲突
- calloc默认0值,空地址