算法系列15天速成 五大经典查找

在我们的算法中,有一种叫做线性查找。分为:顺序查找和折半查找。

查找有两种形态:

分为:破坏性查找,   比如有一群mm,我猜她们的年龄,第一位猜到了是23+,此时这位mm已经从我脑海里面的mmlist中remove掉了。所以此种查找破坏了原来的结构。

       非破坏性查找, 这种就反之了,不破坏结构。

一、顺序查找

   这种非常简单,就是过一下数组,一个一个的比,找到为止。

复制代码 代码如下:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace Sequential
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> list = new List<int>() { 2, 3, 5, 8, 7 };

            var result = SequenceSearch(list, 3);

            if (result != -1)
                Console.WriteLine(“3 已经在数组中找到,索引位置为:” + result);
            else
                Console.WriteLine(“呜呜,没有找到!”);

            Console.Read();
        }

        //顺序查找
        static int SequenceSearch(List<int> list, int key)
        {
            for (int i = 0; i < list.Count; i++)
            {
                //查找成功,返回序列号
                if (key == list[i])
                    return i;
            }
            //未能查找,返回-1
            return -1;
        }
    }
}

《算法系列15天速成 五大经典查找》

二、折半查找:

       这种查找很有意思,就是每次都砍掉一半, 比如”幸运52“中的猜价格游戏,价格在999元以下,1分钟之内能猜到几样给几样,如果那些选手都知道折半查找,
             那结果是相当的啊。

       不过要注意,这种查找有两个缺点:

            第一: 数组必须有序,不是有序就必须让其有序,大家也知道最快的排序也是NLogN的,所以…..呜呜。
            第二: 这种查找只限于线性的顺序存储结构。

上代码:

复制代码 代码如下:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace BinarySearch
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> list = new List<int>() { 3, 7, 9, 10, 11, 24, 45, 66, 77 };

            var result = BinarySearch(list, 45);

            if (result != -1)
                Console.WriteLine(“45 已经在数组中找到,索引位置为:” + result);
            else
                Console.WriteLine(“呜呜,没有找到!”);

            Console.Read();
        }

        ///<summary>
/// 折半查找
///</summary>
///<param name=”list”></param>
///<returns></returns>
        public static int BinarySearch(List<int> list, int key)
        {
            //最低线
            int low = 0;

            //最高线
            int high = list.Count – 1;

            while (low <= high)
            {
                //取中间值
                var middle = (low + high) / 2;

                if (list[middle] == key)
                {
                    return middle;
                }
                else
                    if (list[middle] > key)
                    {
                        //下降一半
                        high = middle – 1;
                    }
                    else
                    {
                        //上升一半
                        low = middle + 1;
                    }
            }
            //未找到
            return -1;
        }
    }
}

《算法系列15天速成 五大经典查找》

先前也说过,查找有一种形态是破坏性的,那么对于线性结构的数据来说很悲惨,因为每次破坏一下,

可能都导致数组元素的整体前移或后移。

    所以线性结构的查找不适合做破坏性操作,那么有其他的方法能解决吗?嗯,肯定有的,不过要等下一天分享。

ps:  线性查找时间复杂度:O(n);
         折半无序(用快排活堆排)的时间复杂度:O(NlogN)+O(logN);
         折半有序的时间复杂度:O(logN);

三、哈希查找:

    对的,他就是哈希查找,说到哈希,大家肯定要提到哈希函数,呵呵,这东西已经在我们脑子里面形成
固有思维了。大家一定要知道“哈希“中的对应关系。
     比如说: ”5“是一个要保存的数,然后我丢给哈希函数,哈希函数给我返回一个”2″,那么此时的”5“
和“2”就建立一种对应关系,这种关系就是所谓的“哈希关系”,在实际应用中也就形成了”2“是key,”5“是value。
    那么有的朋友就会问如何做哈希,首先做哈希必须要遵守两点原则:
          ①:  key尽可能的分散,也就是我丢一个“6”和“5”给你,你都返回一个“2”,那么这样的哈希函数不尽完美。
          ②: 哈希函数尽可能的简单,也就是说丢一个“6”给你,你哈希函数要搞1小时才能给我,这样也是不好的。

其实常用的做哈希的手法有“五种”:
第一种:”直接定址法“。
                  很容易理解,key=Value+C; 这个“C”是常量。Value+C其实就是一个简单的哈希函数。
第二种:“除法取余法”。
                  很容易理解, key=value%C;解释同上。
第三种:“数字分析法”。
                  这种蛮有意思,比如有一组value1=112233,value2=112633,value3=119033,
                  针对这样的数我们分析数中间两个数比较波动,其他数不变。那么我们取key的值就可以是
                  key1=22,key2=26,key3=90。
第四种:“平方取中法”。此处忽略,见名识意。
第五种:“折叠法”。
                 这种蛮有意思,比如value=135790,要求key是2位数的散列值。那么我们将value变为13+57+90=160,
                 然后去掉高位“1”,此时key=60,哈哈,这就是他们的哈希关系,这样做的目的就是key与每一位value都相
                 关,来做到“散列地址”尽可能分散的目地。

正所谓常在河边走,哪有不湿鞋。哈希也一样,你哈希函数设计的再好,搞不好哪一次就撞楼了,那么抛给我们的问题
就是如果来解决“散列地址“的冲突。

其实解决冲突常用的手法也就2种:

第一种: “开放地址法“。
                 所谓”开放地址“,其实就是数组中未使用的地址。也就是说,在发生冲突的地方,后到的那个元素(可采用两种方式
                 :①线性探测,②函数探测)向数组后寻找”开放地址“然后把自己插进入。

第二种:”链接法“。
                这个大家暂时不懂也没关系,我就先介绍一下原理,就是在每个元素上放一个”指针域“,在发生冲突的地方,后到的那
               个元素将自己的数据域抛给冲突中的元素,此时冲突的地方就形成了一个链表。

上面啰嗦了那么多,也就是想让大家在”设计哈希“和”解决冲突“这两个方面提一点参考和手段。

那么下面就上代码了,
     设计函数采用:”除法取余法“。
     冲突方面采用:”开放地址线性探测法”。

复制代码 代码如下:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace HashSearch
{
    class Program
    {
        //“除法取余法”
        static int hashLength = 13;

        //原数据
        static List<int> list = new List<int>() { 13, 29, 27, 28, 26, 30, 38 };

        //哈希表长度
        static int[] hash = new int[hashLength];

        static void Main(string[] args)
        {
            //创建hash
            for (int i = 0; i < list.Count; i++)
            {
                InsertHash(hash, hashLength, list[i]);
            }

            Console.WriteLine(“Hash数据:” + string.Join(“,”, hash));

            while (true)
            {
                Console.WriteLine(“\n请输入要查找数字:”);
                int result = int.Parse(Console.ReadLine());
                var index = SearchHash(hash, hashLength, result);

                if (index != -1)
                    Console.WriteLine(“数字” + result + “在索引的位置是:” + index);
                else
                    Console.WriteLine(“呜呜,” + result + ” 在hash中没有找到!”);

            }
        }

        ///<summary>
/// Hash表检索数据
///</summary>
///<param name=”dic”></param>
///<param name=”hashLength”></param>
///<param name=”key”></param>
///<returns></returns>
        static int SearchHash(int[] hash, int hashLength, int key)
        {
            //哈希函数
            int hashAddress = key % hashLength;

            //指定hashAdrress对应值存在但不是关键值,则用开放寻址法解决
            while (hash[hashAddress] != 0 && hash[hashAddress] != key)
            {
                hashAddress = (++hashAddress) % hashLength;
            }

            //查找到了开放单元,表示查找失败
            if (hash[hashAddress] == 0)
                return -1;
            return hashAddress;

        }

        ///<summary>
///数据插入Hash表
///</summary>
///<param name=”dic”>哈希表</param>
///<param name=”hashLength”></param>
///<param name=”data”></param>
        static void InsertHash(int[] hash, int hashLength, int data)
        {
            //哈希函数
            int hashAddress = data % 13;

            //如果key存在,则说明已经被别人占用,此时必须解决冲突
            while (hash[hashAddress] != 0)
            {
                //用开放寻址法找到
                hashAddress = (++hashAddress) % hashLength;
            }

            //将data存入字典中
            hash[hashAddress] = data;
        }
    }
}

结果:

《算法系列15天速成 五大经典查找》

四、索引查找:
     一提到“索引”,估计大家第一反应就是“数据库索引”,对的,其实主键建立“索引”,就是方便我们在海量数据中查找。
关于“索引”的知识,估计大家都比我清楚,我就简单介绍下。
我们自己写算法来实现索引查找时常使用的三个术语:
第一:主表,      这个很简单,要查找的对象。
第二:索引项,   一般我们会用函数将一个主表划分成几个子表,每个子表建立一个索引,这个索引叫做索引项。
第三:索引表,    索引项的集合也就是索引表。

一般“索引项”包含三种内容:index,start,length

第一: index,也就是索引指向主表的关键字。
第二:start, 也就是index在主表中的位置。
第三:length, 也就是子表的区间长度。

复制代码 代码如下:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace IndexSearchProgram
{
    class Program
    {
        ///<summary>
/// 索引项实体
///</summary>
        class IndexItem
        {
            //对应主表的值
            public int index;
            //主表记录区间段的开始位置
            public int start;
            //主表记录区间段的长度
            public int length;
        }

        static void Main(string[] args)
        {
            Console.WriteLine(“原数据为:” + string.Join(“,”, students));

            int value = 205;

            Console.WriteLine(“\n插入数据” + value);

            //将205插入集合中,过索引
            var index = insert(value);

            //如果插入成功,获取205元素所在的位置
            if (index == 1)
            {
                Console.WriteLine(“\n插入后数据:” + string.Join(“,”, students));
                Console.WriteLine(“\n数据元素:205在数组中的位置为 ” + indexSearch(205) + “位”);
            }

            Console.ReadLine();
        }

        ///<summary>
/// 学生主表
///</summary>
        static int[] students = {
                                   101,102,103,104,105,0,0,0,0,0,
                                   201,202,203,204,0,0,0,0,0,0,
                                   301,302,303,0,0,0,0,0,0,0
                                };
        ///<summary>
///学生索引表
///</summary>
        static IndexItem[] indexItem = {
                                  new IndexItem(){ index=1, start=0, length=5},
                                  new IndexItem(){ index=2, start=10, length=4},
                                  new IndexItem(){ index=3, start=20, length=3},
                                };

        ///<summary>
/// 查找数据
///</summary>
///<param name=”key”></param>
///<returns></returns>
        public static int indexSearch(int key)
        {
            IndexItem item = null;

            // 建立索引规则
            var index = key / 100;

            //首先去索引找
            for (int i = 0; i < indexItem.Count(); i++)
            {
                if (indexItem[i].index == index)
                {
                    item = new IndexItem() { start = indexItem[i].start, length = indexItem[i].length };
                    break;
                }
            }

            //如果item为null,则说明在索引中查找失败
            if (item == null)
                return -1;

            for (int i = item.start; i < item.start + item.length; i++)
            {
                if (students[i] == key)
                {
                    return i;
                }
            }
            return -1;
        }

        ///<summary>
/// 插入数据
///</summary>
///<param name=”key”></param>
///<returns></returns>
        public static int insert(int key)
        {
            IndexItem item = null;
            //建立索引规则
            var index = key / 100;
            int i = 0;
            for (i = 0; i < indexItem.Count(); i++)
            {
                //获取到了索引
                if (indexItem[i].index == index)
                {
                    item = new IndexItem()
                    {
                        start = indexItem[i].start,
                        length = indexItem[i].length
                    };
                    break;
                }
            }
            if (item == null)
                return -1;
            //更新主表
            students[item.start + item.length] = key;
            //更新索引表
            indexItem[i].length++;
            return 1;
        }
    }
}

结果:

《算法系列15天速成 五大经典查找》

ps: 哈希查找时间复杂度O(1)。

       索引查找时间复杂度:就拿上面的Demo来说是等于O(n/3)+O(length)

大家是否感觉到,树在数据结构中大行其道,什么领域都要沾一沾,碰一碰。

就拿我们前几天学过的排序就用到了堆和今天讲的”二叉排序树“,所以偏激的说,掌握的树你就是牛人了。

五、二叉排序树

1. 概念:

     <1> 其实很简单,若根节点有左子树,则左子树的所有节点都比根节点小。

                             若根节点有右子树,则右子树的所有节点都比根节点大。

     <2> 如图就是一个”二叉排序树“,然后对照概念一比较比较。

         《算法系列15天速成 五大经典查找》

2.实际操作:

    我们都知道,对一个东西进行操作,无非就是增删查改,接下来我们就聊聊其中的基本操作。

    <1> 插入:相信大家对“排序树”的概念都清楚了吧,那么插入的原理就很简单了。

                    比如说我们插入一个20到这棵树中。

                                 首先:20跟50比,发现20是老小,不得已,得要归结到50的左子树中去比较。

                                 然后:20跟30比,发现20还是老小。

                              再然后:20跟10比,发现自己是老大,随即插入到10的右子树中。

                                 最后: 效果呈现图如下:

               

               《算法系列15天速成 五大经典查找》

    <2>查找:相信懂得了插入,查找就跟容易理解了。

                    就拿上面一幅图来说,比如我想找到节点10.

                                     首先:10跟50比,发现10是老小,则在50的左子树中找。

                                     然后:10跟30比,发现还是老小,则在30的左子树中找。

                                  再然后:  10跟10比,发现一样,然后就返回找到的信号。

                

     <3>删除:删除节点在树中还是比较麻烦的,主要有三种情况。

                   《1》 删除的是“叶节点20“,这种情况还是比较简单的,删除20不会破坏树的结构。如图:

                    

                      《算法系列15天速成 五大经典查找》

                   《2》删除”单孩子节点90“,这个情况相比第一种要麻烦一点点,需要把他的孩子顶上去。

                    

                       《算法系列15天速成 五大经典查找》

                   《3》删除“左右孩子都有的节点50”,这个让我在代码编写上纠结了好长时间,问题很直白,

                           我把50删掉了,谁顶上去了问题,是左孩子呢?还是右孩子呢?还是另有蹊跷?这里我就

                           坦白吧,不知道大家可否知道“二叉树”的中序遍历,不过这个我会在后面讲的,现在可以当

                          公式记住吧,就是找到右节点的左子树最左孩子。

                          比如:首先 找到50的右孩子70。

                                  然后  找到70的最左孩子,发现没有,则返回自己。

                                  最后  原始图和最终图如下。 

 《算法系列15天速成 五大经典查找》 《算法系列15天速成 五大经典查找》

 

3.说了这么多,上代码说话。

复制代码 代码如下:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Diagnostics;

namespace TreeSearch
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> list = new List<int>() { 50, 30, 70, 10, 40, 90, 80 };

            //创建二叉遍历树
            BSTree bsTree = CreateBST(list);

            Console.Write(“中序遍历的原始数据:”);

            //中序遍历
            LDR_BST(bsTree);

            Console.WriteLine(“\n—————————————————————————n”);

            //查找一个节点
            Console.WriteLine(“\n10在二叉树中是否包含:” + SearchBST(bsTree, 10));

            Console.WriteLine(“\n—————————————————————————n”);

            bool isExcute = false;

            //插入一个节点
            InsertBST(bsTree, 20, ref isExcute);

            Console.WriteLine(“\n20插入到二叉树,中序遍历后:”);

            //中序遍历
            LDR_BST(bsTree);

            Console.WriteLine(“\n—————————————————————————n”);

            Console.Write(“删除叶子节点 20, \n中序遍历后:”);

            //删除一个节点(叶子节点)
            DeleteBST(ref bsTree, 20);

            //再次中序遍历
            LDR_BST(bsTree);

            Console.WriteLine(“\n****************************************************************************\n”);

            Console.WriteLine(“删除单孩子节点 90, \n中序遍历后:”);

            //删除单孩子节点
            DeleteBST(ref bsTree, 90);

            //再次中序遍历
            LDR_BST(bsTree);

            Console.WriteLine(“\n****************************************************************************\n”);

            Console.WriteLine(“删除根节点 50, \n中序遍历后:”);
            //删除根节点
            DeleteBST(ref bsTree, 50);

            LDR_BST(bsTree);

        }

        ///<summary>
/// 定义一个二叉排序树结构
///</summary>
        public class BSTree
        {
            public int data;
            public BSTree left;
            public BSTree right;
        }

        ///<summary>
/// 二叉排序树的插入操作
///</summary>
///<param name=”bsTree”>排序树</param>
///<param name=”key”>插入数</param>
///<param name=”isExcute”>是否执行了if语句</param>
        static void InsertBST(BSTree bsTree, int key, ref bool isExcute)
        {
            if (bsTree == null)
                return;

            //如果父节点大于key,则遍历左子树
            if (bsTree.data > key)
                InsertBST(bsTree.left, key, ref isExcute);
            else
                InsertBST(bsTree.right, key, ref isExcute);

            if (!isExcute)
            {
                //构建当前节点
                BSTree current = new BSTree()
                  {
                      data = key,
                      left = null,
                      right = null
                  };

                //插入到父节点的当前元素
                if (bsTree.data > key)
                    bsTree.left = current;
                else
                    bsTree.right = current;

                isExcute = true;
            }

        }

        ///<summary>
/// 创建二叉排序树
///</summary>
///<param name=”list”></param>
        static BSTree CreateBST(List<int> list)
        {
            //构建BST中的根节点
            BSTree bsTree = new BSTree()
            {
                data = list[0],
                left = null,
                right = null
            };

            for (int i = 1; i < list.Count; i++)
            {
                bool isExcute = false;
                InsertBST(bsTree, list[i], ref isExcute);
            }
            return bsTree;
        }

        ///<summary>
/// 在排序二叉树中搜索指定节点
///</summary>
///<param name=”bsTree”></param>
///<param name=”key”></param>
///<returns></returns>
        static bool SearchBST(BSTree bsTree, int key)
        {
            //如果bsTree为空,说明已经遍历到头了
            if (bsTree == null)
                return false;

            if (bsTree.data == key)
                return true;

            if (bsTree.data > key)
                return SearchBST(bsTree.left, key);
            else
                return SearchBST(bsTree.right, key);
        }

        ///<summary>
/// 中序遍历二叉排序树
///</summary>
///<param name=”bsTree”></param>
///<returns></returns>
        static void LDR_BST(BSTree bsTree)
        {
            if (bsTree != null)
            {
                //遍历左子树
                LDR_BST(bsTree.left);

                //输入节点数据
                Console.Write(bsTree.data + “”);

                //遍历右子树
                LDR_BST(bsTree.right);
            }
        }

        ///<summary>
/// 删除二叉排序树中指定key节点
///</summary>
///<param name=”bsTree”></param>
///<param name=”key”></param>
        static void DeleteBST(ref BSTree bsTree, int key)
        {
            if (bsTree == null)
                return;

            if (bsTree.data == key)
            {
                //第一种情况:叶子节点
                if (bsTree.left == null && bsTree.right == null)
                {
                    bsTree = null;
                    return;
                }
                //第二种情况:左子树不为空
                if (bsTree.left != null && bsTree.right == null)
                {
                    bsTree = bsTree.left;
                    return;
                }
                //第三种情况,右子树不为空
                if (bsTree.left == null && bsTree.right != null)
                {
                    bsTree = bsTree.right;
                    return;
                }
                //第四种情况,左右子树都不为空
                if (bsTree.left != null && bsTree.right != null)
                {
                    var node = bsTree.right;

                    //找到右子树中的最左节点
                    while (node.left != null)
                    {
                        //遍历它的左子树
                        node = node.left;
                    }

                    //交换左右孩子
                    node.left = bsTree.left;

                    //判断是真正的叶子节点还是空左孩子的父节点
                    if (node.right == null)
                    {
                        //删除掉右子树最左节点
                        DeleteBST(ref bsTree, node.data);

                        node.right = bsTree.right;
                    }
                    //重新赋值一下
                    bsTree = node;

                }
            }

            if (bsTree.data > key)
            {
                DeleteBST(ref bsTree.left, key);
            }
            else
            {
                DeleteBST(ref bsTree.right, key);
            }
        }
    }
}

运行结果:

《算法系列15天速成 五大经典查找》

值的注意的是:二叉排序树同样采用“空间换时间”的做法。

突然发现,二叉排序树的中序遍历同样可以排序数组,呵呵,不错!

PS:  插入操作:O(LogN)。
       删除操作:O(LogN)。
       查找操作:O(LogN)。

    原文作者:五大常用算法
    原文地址: https://blog.csdn.net/lanmolei814/article/details/44903699
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞