(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找

目录

引言:     

一、顺序查找(Sequential Search)

1.概要

2.查找过程

3.算法实现

(1).以顺序表作为存储结构,实现顺序查找算法

数据元素类型定义:

顺序表的定义:

实现主函数:

哨兵函数:

完整代码示例:

(2).以链表作为存储结构,实现顺序查找算法

链表节点的定义:

初始化链表:

实现顺序查找算法:

完整代码示例:

4.算法分析

5.顺序查找优缺点总结

二、折半查找(二分查找)(Binary Search)

1.概要

2.查找过程

3.算法实现

(1).递归实现折半查找

(2).迭代实现折半查找        

(3).完整代码示例

4.例题演示

5.算法分析(含判定树概念)

6.折半查找优缺点总结

三、分块查找(索引顺序查找)(Blocking Search)

1.概要

2.查找过程

3.例题演示

4.算法分析

5.分块查找的优缺点总结

四、总结


引言:     

        线性表是最基础的数据结构,它由一系列具有相同数据类型的元素组成,这些元素在内存中连续存放。在线性表中查找特定的元素,通常有以下几种方法:

        1、顺序查找(Sequential Search)

        2、折半查找(二分查找)(Binary Search)

        3、分块查找(索引顺序查找)(Blocking Search)

一、顺序查找(Sequential Search)

1.概要

        顺序查找又称线性查找,适用于顺序表和链表。对于顺序表,可通过数组下标递增顺序扫描每个元素;对于链表,可通过指针next来依次扫描每个元素。

2.查找过程

        从线性表的一端开始,依次将记录的关键字与给定值进行比较,也就是逐个检查关键字是否满足给定的条件。

        若查找到某个记录的关键字和给定值相等,则查找成功;反之,查找失败。

3.算法实现

(1).以顺序表作为存储结构,实现顺序查找算法

  • 数据元素类型定义:
typedef struct {
	KeyType key;	//关键字域
	InfoType otherinfo;		//其他域
}ElemType;
  • 顺序表的定义:
typedef struct {
	ElemType* R;	//存储空间基地址
	int length;		//当前长度
};
  • 实现主函数:
int Search_Seq(SSTable ST, KeyType key) {
	//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为该元素在表中的位置否则为0
	int i = 0;
	for (i = ST.length; i >= 1; --i) {
		if (ST.R[i].key == key) {
			return i;
		}
	}
	return 0;
}

        在查找过程中,每执行一次for循环内的代码都要检查i是否>=1,也就是判断整个表是否检查完毕,从而增加了程序执行的时间及任务量。

        对此将ST.elem[0]设为哨兵,引用它的目的是使得Search_Seq内的循环不必判断数组是否会越界。

        因此将原来算法每执行一次for循环内的代码,需要判断两次(i>=1)(ST.R[i]key==key),变为了每执行一次for循环内的代码仅需判断一次(ST.R[i]key!=key)

        即将上面的实现主函数代码替换为下面的哨兵函数代码。

  • 哨兵函数:
int Search_Seq(SSTable ST, KeyType key) {
	//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为该元素在表中的位置否则为0
	int i = 0;
	ST.R[0].key = key;//“监视哨”
	for (i = ST.length; ST.R[i].key != key; --i);//从后往前查找
	return i;
}

        在程序中引入“哨兵”并不是这个算法独有的,引入“哨兵”可以避免很多不必要的判断语句,从而提高程序效率。

  • 完整代码示例:
#include 
#include 

typedef char InfoType;
typedef int KeyType;

// 定义结构体ElemType,包含关键字域和其他域
typedef struct {
    KeyType key;        // 关键字域
    InfoType otherinfo;  // 其他域
} ElemType;

// 定义结构体SSTable,表示顺序表
typedef struct {
    ElemType* R;        // 存储空间基地址
    int length;         // 当前长度
} SSTable;

// 函数Search_Seq在顺序表ST中顺序查找关键字等于key的数据元素
int Search_Seq(SSTable ST, KeyType key) {
    int i = 0;
    ST.R[0].key = key;
    for (i = ST.length; ST.R[i].key != key; --i); // 从后往前查找
    return i;
}

int main() {
    // 假设KeyType是int,InfoType是char
    typedef int KeyType;
    typedef char InfoType;

    // 创建一个顺序表
    SSTable ST;
    ST.R = (ElemType*)malloc(10 * sizeof(ElemType)); // 假设分配10个元素的空间
    ST.length = 0;

    // 向顺序表中添加元素
    ST.R[1].key = 1;
    ST.R[1].otherinfo = 'A';
    ST.R[2].key = 2;
    ST.R[2].otherinfo = 'B';
    ST.length = 3;

    // 调用Search_Seq函数查找关键字为2的元素
    int position = Search_Seq(ST,2);
    if (position != 0) {
        printf("Element found at position %d\n", position);
        printf("%d", ST.R[position].otherinfo);
        putchar(ST.R[position].otherinfo);//将ASCII码转换为字符
    }
    else {
        printf("Element not found\n");
    }

    // 释放顺序表空间
    free(ST.R);

    return 0;
}

(2).以链表作为存储结构,实现顺序查找算法

  • 链表节点的定义:

        该个链表节点包含两部分:数据域和指针域。

        数据域用于存储节点的数据,指针域用于存储指向下一个节点的指针。

typedef struct ListNode {
    int data;           // 数据域
    struct ListNode* next; // 指针域,指向下一个节点
} ListNode;
  • 初始化链表:

        初始化链表时,我们需要创建一个头节点,并设置指针域为NULL,表示链表为空。

ListNode* InitializeList() {
    ListNode* head = (ListNode*)malloc(sizeof(ListNode));
    if (head != NULL) {
        head->data = 0; // 头节点数据域初始化为0或其他特殊值
        head->next = NULL; // 头节点指针域初始化为NULL
    }
    return head;
}
  • 实现顺序查找算法:

        我们对本文顺序查找算法通过对链表进行遍历来实现。

        给定一个目标值,我们从链表的头节点开始遍历,逐个检查每个节点的数据域,直到找到目标值或遍历完所有节点。

ListNode* SequentialSearch(ListNode* head, int value) {
    ListNode* current = head; // 从头节点开始
    while (current != NULL) {
        if (current->data == value) { // 检查当前节点的数据域是否等于目标值
            return current; // 如果是,返回当前节点
        }
        current = current->next; // 否则,移动到下一个节点
    }
    return NULL; // 如果没有找到,返回NULL
}
  • 完整代码示例:
#include 
#include 

typedef struct ListNode {
    int data;
    struct ListNode* next;
} ListNode;

ListNode* InitializeList() {
    ListNode* head = (ListNode*)malloc(sizeof(ListNode));
    if (head != NULL) {
        head->data = 0;
        head->next = NULL;
    }
    return head;
}

ListNode* SequentialSearch(ListNode* head, int value) {
    ListNode* current = head;
    while (current != NULL) {
        if (current->data == value) {
            return current;
        }
        current = current->next;
    }
    return NULL;
}

int main() {
    ListNode* head = InitializeList();
    ListNode* node1 = (ListNode*)malloc(sizeof(ListNode));
    ListNode* node2 = (ListNode*)malloc(sizeof(ListNode));

    head->next = node1;
    node1->data = 1;
    node1->next = node2;
    node2->data = 2;
    node2->next = NULL;

    ListNode* result = SequentialSearch(head, 2);
    if (result != NULL) {
        printf("Found node with value %d\n", result->data);
    } else {
        printf("Node with value %d not found\n", 2);
    }

    // 释放链表内存等操作...

    return 0;
}

4.算法分析

        对于有n个元素的表,给定值key与表中第i个元素相等,即定位第i个元素时,需进行n-i+1次关键字的比较,即.

        查找成功时,顺序查找的平均长度为:

(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第1张图片

        当每个元素的查找概率相等,即时,有

(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第2张图片

        查找不成功时,与表中各个关键字的比较次数是n+1次,即

        因此监视哨函数时间复杂度为O(n)

        空间复杂度为O(1),因为监视哨利用了一个额外的辅助存储空间。

5.顺序查找优缺点总结

        优点:算法简单,对表结构无任何要求,顺序存储或链式存储皆可。同时对表中记录的有序性也没有要求,无论记录是否按关键字有序,均可应用。需注意的是,对线性的链表只能进行顺序查找。

        缺点:平均查找长度较大,查找效率较低,当n较大时,不适合采用顺序查找。


二、折半查找(二分查找)(Binary Search)

1.概要

        折半查找(Binary Search)也称二分查找,它要求线性表必须采用顺序存储结构,即有序的顺序表。

2.查找过程

        从表中间记录开始,如果给定值key和中间记录的关键字相等,则查找成功;如果给定值key比中间记录的关键字大或小,则在表中大或小的中间记录的那一半中继续查找,重复这样的操作,直至查找成功,或确定表中没有所需要查找的元素,则查找失败,返回失败信息。

3.算法实现

(1).递归实现折半查找

// 递归实现折半查找
int binarySearchRecursive(int arr[], int left, int right, int target) {
    if (left > right) {
        return -1;  // 搜索失败,返回-1
    }
    int mid = (left + right) / 2;  // 计算中间位置
    if (arr[mid] == target) {
        return mid;  // 搜索成功,返回目标元素的位置
    }
    else if (arr[mid] < target) {
        return binarySearchRecursive(arr, mid + 1, right, target);  // 在右半部分继续查找
    }
    else {
        return binarySearchRecursive(arr, left, mid - 1, target);  // 在左半部分继续查找
    }
}

(2).迭代实现折半查找        

// 迭代实现折半查找
int binarySearchIterative(int arr[], int target) {
    int left = 0, right = sizeof(arr) - 1;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (arr[mid] == target) {
            return mid;  // 搜索成功,返回目标元素的位置
        }
        else if (arr[mid] < target) {
            left = mid + 1;  // 在右半部分继续查找
        }
        else {
            right = mid - 1;  // 在左半部分继续查找
        }
    }
    return -1;  // 搜索失败,返回-1
}

(3).完整代码示例

#include 

// 递归实现折半查找
int binarySearchRecursive(int arr[], int left, int right, int target) {
    if (left > right) {
        return -1;  // 搜索失败,返回-1
    }
    int mid = (left + right) / 2;  // 计算中间位置
    if (arr[mid] == target) {
        return mid;  // 搜索成功,返回目标元素的位置
    }
    else if (arr[mid] < target) {
        return binarySearchRecursive(arr, mid + 1, right, target);  // 在右半部分继续查找
    }
    else {
        return binarySearchRecursive(arr, left, mid - 1, target);  // 在左半部分继续查找
    }
}

// 迭代实现折半查找
int binarySearchIterative(int arr[], int target) {
    int left = 0, right = sizeof(arr) - 1;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (arr[mid] == target) {
            return mid;  // 搜索成功,返回目标元素的位置
        }
        else if (arr[mid] < target) {
            left = mid + 1;  // 在右半部分继续查找
        }
        else {
            right = mid - 1;  // 在左半部分继续查找
        }
    }
    return -1;  // 搜索失败,返回-1
}

// 主函数,用于测试折半查找
int main() {
    int arr[] = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 };  // 示例数组
    int target = 8;  // 要查找的目标值
    int n = sizeof(arr) / sizeof(arr[0]);  // 数组的长度

    // 测试递归实现
    int resultRecursive = binarySearchRecursive(arr, 0, n - 1, target);
    if (resultRecursive != -1) {
        printf("Element %d found at index %d using recursive binary search.\n", target, resultRecursive);
    }
    else {
        printf("Element %d not found using recursive binary search.\n", target);
    }

    // 测试迭代实现
    int resultIterative = binarySearchIterative(arr, target);
    if (resultIterative != -1) {
        printf("Element %d found at index %d using iterative binary search.\n", target, resultIterative);
    }
    else {
        printf("Element %d not found using iterative binary search.\n", target);
    }

    return 0;
}

4.例题演示

        已知包含10个数据元素的有序表(关键字即数据元素的值):

( 1, 3, 5, 7, 9, 11, 13, 15, 17, 19)

请给出查找关键字为8的数据元素的折半查找过程。(下面仅展示图片流程)


1 3 5 7 9 11 13 15 17 19

                        ⬆                                      ⬆                                                           ⬆

                     left=0                               mid=4                                                    right=9


1 3 5 7 9 11 13 15 17 19

                        ⬆        ⬆                 ⬆

                     left=0 mid=1          right=3


1 3 5 7 9 11 13 15 17 19

                                            ⬆       ⬆

                                       left=2  right=3

                                        mid=2


1 3 5 7 9 11 13 15 17 19

                                                     ⬆

                                                  right=3

                                                  left=3

                                                  mid=3


1 3 5 7 9 11 13 15 17 19

                                                     ⬆        ⬆

                                                  right=3  left=4

                                                  mid=3


5.算法分析(含判定树概念)

        在这引入判定树的概念,判定树:把当前查找区间的中间位置作为根,把左子表和右子表分别作为根的左子树和右子树,由此得到的二叉树称为折半查找的判定树。

        

(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第3张图片

        由此可知,折半查找在查找成功时进行比较的关键字个数最多不超过树的深度。而判定树的形态只与表记录个数n相关,与关键字的取值无关,具有n个结点的判定树的深度至多为

        图中方型结点为判定树的外部结点,圆形结点为判定树的内部节点。

        给定方型结点的目的是,当折半查找失败时,和给定值进行比较的关键字个数最多也不超过

        下面我们进行折半查找平均查找长度的推理:

        假定有序表的长度为:,则判定树是深度为的满二叉树。

        树中层次为1的结点有1个,层次为2时 的结点有3个,依次类推,层次为h时的结点有个。

        假设表中每个记录的查找概率相等(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第4张图片,则查找成功时折半查找的平均查找长度为:

(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第5张图片

(注:j是树的深度,2的(j-1)次方是每层多少个结点)

        当n较大时,由高数极限相关知识可得:

        由此得出,折半查找的时间复杂度:

        空间复杂度为:O(1)

6.折半查找优缺点总结

        优点:折半查找效率高于顺序查找,比较次数少,查找效率高。

        缺点:只能用于顺序存储的有序表。(折半查找前需要对元素进行排序,如果元素量过大,排序也会消耗大量时力;若插入或删除表中元素,平均比较和移动表中一半的元素也会消耗大量时力。所以折半排序不适用于数据元素经常变动的线性表)


三、分块查找(索引顺序查找)(Blocking Search)

1.概要

        分块查找又称索引顺序查找,它吸取了顺序查找和折半查找的优点,既有动态结构,又适于快速查找。

2.查找过程

        分块查找与前两种方法相比,需额外建立一个“索引表”。

        将查找表分为若干子表(或称块),对每个子表建立一个索引项,其中包含两项内容:关键字项(其值为该子表内的最大)和指针项(指示该子表的第一个记录在表中的位置)。每个索引项构成一个索引表,索引表按关键字有序排列。

        块内的元素可以无序,但块间的元素是有序的,即第一块中最大关键字小于第二块中的所有记录的关键字,第二块中最大关键字小于第三块中的所有记录的关键字,以此类推。

3.例题演示

        

(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第6张图片

        分块查找过程需分两步进行,

        第一步,先确定待查找记录所在的块(子表);第二步,在块中顺序查找。

        假设给定的值key=27,则先将key依次和索引表中各分块的最大关键字进行比较,因为22

        因为第二个子表的指针指向第二个子表的第一个记录是表中第4个记录,则从第4个记录进行顺序查找,直到ST.elem[10].key=key为止。

        假如子表没有关键字等于key的记录,则查找不成功。

        因此可见,分块查找算法为顺序查找与折半查找两个算法简单合成。

4.算法分析

分块查找的平均查找长度为:

其中,Lb为查找索引表确定所在块的平均长度,Lw为在块中查找元素的平均查找长度。

        我们将长度为n的表均匀的分成b块,每块含有s个记录,即;

        又假定表中每个记录的查找概率相等,则每块查找的概率为1/b,块中每个记录的查找概率为1/s。

  • 若用顺序查找确定所在块,则分块查找的平均查找长度为:

(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第7张图片

        从上述表达式分析可得,顺序查找确定所在块,与s,n均有关系。因此证得,当s取时,

取最小值+1.

  • 若用折半查找确定所在块,则分块查找的平均查找长度为:

(详解)数据结构线性表的查找——顺序查找、折半查找、分块查找_第8张图片


时间复杂度:
        分块查找的时间复杂度主要取决于块的数量以及每个块中元素的数量。在最坏的情况下,即目标元素位于最后一个块中,并且最后一个块中的元素数量最多,分块查找需要遍历所有块,并且对于最后一个块,需要遍历其中的所有元素。因此,时间复杂度为 O(m + n),其中 m 是块的数量,n 是所有块中元素的总数。
空间复杂度:
        分块查找的空间复杂度主要取决于用于存储块的辅助空间。由于每个块中的元素是无序的,所以无法通过块中的元素来直接定位目标元素,需要额外的空间来存储块的索引。因此,空间复杂度为 O(m),其中 m 是块的数量。这是因为需要存储每个块的索引,而每个块的大小是不确定的,所以不能将空间复杂度表示为与块中元素数量相关的函数。

5.分块查找的优缺点总结

        优点:在表中插入和删除数据元素,只需找到对应的块,就可在该块中进行插入和删除运算。由于块中是无序的,故插入和删除无需进行大量移动。

        缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算。


四、总结

顺序查找 折半查找 分块查找
ASL 最大 最小 中间
表结构 有序表、无需表 有序表 分块有序
存储结构 顺序表、线性链表 顺序表 顺序表、线性链表

        在本文中,我们详细讨论了数据结构线性表的查找算法,包括顺序查找、折半查找和分块查找。这些算法在不同的应用场景和数据特性下有着各自的优势和局限性。
顺序查找是最基本的查找算法,它适用于无序或有序的线性表。尽管在数据量较大时效率较低,但它的实现简单,适用范围广泛。
        折半查找是针对有序线性表的查找算法,它通过不断缩小查找范围来提高查找效率。折半查找的时间复杂度为 O(log n),但需要注意的是,它要求线性表必须是有序的。
分块查找是吸取了顺序查找和折半查找的优点,它通过将数据分块并建立索引来提高查找效率。分块查找在无序数据中表现良好,其时间复杂度为 O(m + n),其中 m 是块的数量,n 是所有块中元素的总数。
        在实际应用中,选择合适的查找算法需要考虑数据的特点和应用场景。对于有序数据,折半查找是最佳选择;对于无序数据,分块查找通常更为高效。此外,随着数据量的增长,查找算法的效率将越来越重要,因此选择合适的数据结构和查找算法对于提高应用性能至关重要。

你可能感兴趣的:(数据结构,数据结构)