[数据结构] 4. 排序与查找

上标x2,下标x0

0.前言

  1. 排序四象限原则:稳定/非稳定、内部/外部
    稳定排序:值相同的元素在排序前后相对位置不变
    内部排序:整体数据加载到内存中进行排序

  2. 本节内容排序均为递增

1. 稳定排序

1.1 插入排序(内部)

在这里插入图片描述

  1. 时间复杂度o(n2),基于比较和交换

1.2 冒泡排序(内部)

  1. 时间复杂度o(n2),基于比较和交换
  2. 优化
    某一轮冒泡过程没有任何交换操作的时候,说明数组有序,结束整个冒泡排序

1.3 归并排序(外部)

  1. 时间复杂度o(nlogn),基于比较

  2. 思考:2个有序数组合并为1个有序数组的方法?
    定义3个指针指向3个数组,比较两个小数组的指针指向的值,插入到大数组中

  3. 分治思想:大规模化小规模,直到可以排序

  4. 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 选择排序

  1. 时间复杂度:o(n2),基于比较和交换
  2. 不稳定排序现象: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. 哈希表是一种数据结构,底层实现用到了数组,是把大的集合分组为小的集合,方便查找
    前面讲的都是算法
  • 举例:电话簿
  • 实现哈希表我们可以采用两种方法:
    1、数组+链表
    2、数组+二叉树

数组:

  1. 数组是展开的函数,函数是压缩的数组,即数组和函数有联系
    共同点是都包含一种映射关系
  2. 查找时间复杂度o(1),因为数组是顺序存储的,通过地址+偏移量可以直接访问
  3. 下标【int】映射为值【任意类型】,这是哈希表实现的灵感来源

哈希表:

  1. 值【任意类型】映射为下标【int】,把值存储在该下标位置
  2. 核心内容:哈希函数、冲突处理方法
  3. 冲突处理方法有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;
}

【亮点】

  1. 针对不同数据,优秀的哈希函数和冲突处理方法不同,主要是哈希函数不同
    对字符串查找时,哈希函数用BKDRhash,冲突处理方法采用拉链法
  2. 哈希表需要一片连续的存储空间存储任意类型,当前任意类型的每一位存的是一个链表
    Node **data;
    *data表示data指向连续存储空间的首地址,每一位是Node *
  3. strdup()把传进来的字符串拷贝一份,返回拷贝后的字符串的新地址
    是一种字符串拷贝库函数,一般和free函数成对出现
  4. 哈希表的利用率50%-90%,工业上达到70%即可,哈希表利用率和冲突函数有关,只要是哈希函数就一定会有冲突
  5. calloc默认0值,空地址
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值