精华内容
下载资源
问答
  • 怎么把小v叫出来
    万次阅读 多人点赞
    2021-03-15 11:30:33

    CSDN 的小伙伴们,大家好,我是沉默王二。

    作为 CSDN 的前排博主(18 万+关注,有点飘了哈),我接触了太多太多想学编程、想把编程学好的人,有从别的专业转过来的,有零基础自学的,有科班出身的。他们当中的一部分人,学着学着就放弃了,或者还在放弃的路上。

    所以真的想掏心掏肺给大家谈一谈,在学好编程这条路上,我们该做好哪些心理准备,该怎么去学。

    01、很遗憾

    我上大学那会,学校的计算机专业刚成立两年,也就是说,我们是第二批。据说,第一批做小白鼠的学长学姐们,很多在毕业的时候都没从事计算机专业方面的工作。倒不是他们不想找这方面的工作,而是本地的工作机会寥寥无几。

    很幸运,我是班里面为数不多坚持下来的。准确来说,这个数不超过 10。剩余的呢?能在大一的时候转专业的都转了,转不了的毕业后都干别的去了。

    2008 年那会,郑州(老家河南洛阳的,老乡可以关注下哈)几乎没有计算机专业方面的工作,于是我们都莫名其妙地丧失了对未来的信心。老师们呢,也不吭声,哪怕说一句,“你们去北上广深闯一闯吧”,没准我们都能成为时代的弄潮儿,毕竟一线城市的工作机会还是挺多的,把握住的话,还真能成就一番作为,毕竟早就是优势啊。

    现在想起来,不仅为我自己感到遗憾,也为班里面的同学们感到遗憾。我们的专业是学校最不看好的,但却是那个时代最有前景的,十多年的时间也证明了这一点。但我们还是因为自身的局限性错过了,错过了去一线城市闯荡的最佳时机。

    以前,程序员稀缺,但机会也少;现在,机会多,程序员也多。每个时代都有每个时代的局限性,那还要学不学编程了呢?

    02、选择比努力更重要

    时不时就会有人问我,“25 岁了,学编程还来得及吗?”“30 岁了,学编程晚吗?”也有人问我,“正在上高中,我想以后从事软件开发,我该学些什么呢?”

    每个人,不管处于什么样的年龄段,都有选择的权力。

    大家应该听过这句话,“互联网时代,选择比努力更重要。”于是,大多数人就在纠结,到底该选择什么才能不那么努力就轻而易举的得到,到底选择什么才能不后悔。

    但大多数人往往会忽略一件事,说“选择比努力更重要”这句话的人,其实付出了很多很多常人看不到的努力

    很多人听说 IT 行业很吃香,于是倔强的选择了 IT,这是对的,互联网虽然已经很卷了,但仍然处在蓬勃发展的阶段,很多地方都充满了机会。

    我要告诉大家的是,既然选择了,就要付出努力,不要轻言放弃。

    很多事情,短时间内都是没有答案的,只有把时间线放得长一点,才能验证选择到底是否正确

    03、编程难吗

    我妹学了有小半年的编程了,她就经常给我抱怨,“编程太难了!”

    PS:非忽悠,真的,大家可以点击链接看我另外一篇文章,送我妹上了大学

    说句实在话,编程确实不是一件容易的事儿,我上大学那会,也觉得编程难,难得想要放弃!真的,一点不骗大家。

    现在不是放寒假了嘛,我就没看见过我妹打开电脑敲过代码,每天用得最多的一个软件,大家不用猜应该就知道,它叫“抖音”。

    抖音是一件国民级的软件,的确给我们带来了很多欢乐和感动,我不讨厌它也不喜欢它,因为对于那些自制力差的人来说,抖音悄无声息地就把他们的时间消耗殆尽。

    卡耐基的《人性的弱点》里阐述了一个发人深省的观点:

    99% 的情况下,不管犯下多么严重的错误,人们都会优先归咎于他人。

    例子我就不再举了,我也有过这样的想法。一开始学编程很难,于是同学们不约而同地都把责任归咎到了老师那边,认为是老师自己学艺不精,所以才没办法把我们教好。

    但事实上,这就是人性的弱点,我们都在想方设法地为自己找借口,找一个看起来很合理的接口。

    编程难不难,我估计没人敢说容易,哪怕是出了名的大佬们。可正因为难,才有价值,不是吗?因为难,就不学了吗?

    04、努力就真的能成功吗

    答案也显而易见,“不一定。”

    我每周会去三次健身房,每次都会见到一个人,他的肚子好像一直就那么大。他没有请私人教练,也没有练习器械,单纯的就是在跑步机上慢走,走大概 20 分钟左右。

    在我看来,他很努力也很自律,然而这样的努力,往往收不到任何的效果。

    这也就是为什么,常常有人会抱怨,“为什么那谁谁谁努力了就成功了,而我没有呢!”

    每天把输出“hello world”的程序敲上一百遍,坚持一百天,这样学编程的话,效果可想而知,是学不好的。努力,并不是不断地做重复性的工作。

    真正的努力,需要花费足够多的时间,并且要不断的寻求突破

    就拿学习 Java 来说吧,一开始可能要先学习 C 语言,打下坚实的基础,如果没有学的话,直接开始学 Java 也行,但以后有时间的话,还是要补一补的,好处很多。从 Java 基础知识,到面向对象编程,到网络编程,到多线程并发,到 Java 虚拟机,到性能优化。

    这一条线下来,还不够。还要学习工具,比如说 IDE、Git、Maven;还要学习框架,比如说 Spring、MyBatis、Spring Boot;还要学习数据库,比如说 MySQL、Redis 等等。

    我在知乎上有一个 3100 多赞的 Java 自学路线,这里推荐给大家。

    自学java,学多久可以自己找到工作?

    PS:说句心里话,在知乎上拿到 3100+ 赞真的不容易,尤其是对于计算机领域的博主来说。真的帮助了许许多多的读者,希望大家不要错过。

    我也同步到了 CSDN 上,大家可以点击链接看一看了。

    拜托,学妹,别再问我怎么自学 Java 了!和盘托出

    除此之外,数据结构与算法、设计模式、计算机网络、计算机组成原理、操作系统等等,这些也要学,也只有这样,不断地走出舒适区,不断地突破,不断地寻求边界,然后才能真正地把编程学好。

    说到设计模式,我这里有一份小傅哥重写的 Java 设计模式,已经下载了2 万多次,同样强烈推荐给大家。可以通过下面的链接获取。

    设计模式,牛逼!

    05、目标如何定

    对,我们常说,做任何事情之前,都要先定个目标,这样做起事情来就有了动力,不至于漫无目的。

    但,如果做什么事情都奔着一个目标去,会感觉很累,累到有时候你会丧失信心。

    我们可以把“我要学好编程”作为目标,也可以把这个目标简单做下拆分,比如说先入门再进阶,然后再拓展。更甚至,可以抱着一种“玩一玩”的心态,也没什么大不了的。

    我喜欢看王小波的书,但一开始,我并不知道有王小波这个人,而在通过一个叫《一个人的书房》的播客节目了解到的。这个播客里提到了《沉默的大多数》,于是我就买来读了读,觉得有趣得不得了,然后我就又买了时代三部曲,然后就读啊读。

    等到我要写作的时候,莫名其妙地,就受到了王小波的影响,写出来的文字就带有一些“幽默风趣”在里面。

    但如果一开始,我是抱着一种我要把文章写得有趣,再去读王小波的作品的话,没准我会读得很困难,因为我可能会静不下心来,我想从书里面挖掘“有趣”,可能就失去了阅读的兴趣。

    同样的,如果我们抱着一种“玩一玩”的心态来学习编程的话,没准真能把编程学好。但如果我们抱着一种“我一定要把编程学好”这种心态的话,没准在遇到困难的时候就放弃了,因为我们定下来的目标很难完成,以至于我们有一种负罪感,这种学习状态下,学好是一件很难的事,学不好倒是一件很容易的事。

    把目标降到最低,也是为什么我们在学习一门编程语言的时候要敲“hello world”的原因

    最后,希望大家都能把编程学好,从一键三连做起吧。

    推荐阅读:

    V4.0 《JavaGuide 面试突击版》来啦!GitHub 上标星 98.1k,帮你成功上岸!

    火爆!GitHub 标星 144k 的前后端学习路线,2021 年最新整理,看完后不再迷茫不再徘徊

    更多相关内容
  • V2V迁移

    千次阅读 2019-01-10 12:12:35
    v2v(virtual machine to virtual machine)是通过各种手段将不同平台的虚拟机进行迁移的一种方式。现在常见的迁移有 VMware –> KVM KVM –> VMware Xen –> VMware 本篇将介绍如何使用工具和...

    前言
    v2v(virtual machine to virtual machine)是通过各种手段将不同平台的虚拟机进行迁移的一种方式。现在常见的迁移有
    VMware –> KVM
    KVM –> VMware
    Xen –> VMware
    本篇将介绍如何使用工具和手动方式来将VMware上虚机迁移到KVM环境中,以及博主在实际操作中遇到一些实际问题的troubleshooting。
    VMware虚拟机迁移到KVM的方式
    分为两种,一种是静态迁移,另一种是在线迁移。
    静态迁移(offline migration)也叫做常规迁移,离线迁移。在迁移之前将虚拟机暂停,同时拷贝虚拟机镜像和状态到目的主机。相比较于在线迁移(online migration),其缺点就是静态迁移方式的过程需要显式的停止虚拟机的运行,而在线迁移的过程仅有非常短暂的停机时间,保证迁移过程中虚拟机服务的持续可用;静态迁移的优点是兼容性强,不需要VMM的同意API接口,可以借助第三方工具辅助迁移。由于目前VMM的开发没有统一的标准,不同的虚拟化厂商,尤其是闭源的厂商开发的VMM各成一套,静态迁移是解决不同类型虚拟机之间迁移的有效办法。我们分别演示使用手动方式将VMware虚拟机迁移到KVM和使用自动化迁移工具virt-v2v方式来迁移。
    VMware虚拟机文件介绍
    成功迁移虚拟机的一个重要的验证标准就是文件的正确性和完整性。下图中列出与 VMware 虚拟机相关的文件,但是迁移 VMware 虚拟机到 KVM 并不需要用到所有的文件。
    在这里插入图片描述
    VMware 虚拟机的文件主要包括 .nvram 文件,.vmx 文件,vmdk 文件,.vswp 文件,.vmss 文件,.vmsd 文件,.vmsn 文件,.log 文件,.vmxf 文件。与迁移相关的最主要的是 .vmx 文件和 .vmdk 文件。

    • .vmx 文件:包括虚拟机所有配置信息与硬件设置。不管你对虚拟机的设置做了何种编辑,所有的信息都会以文本形式保存在这个文件里。如特殊硬件配置(例如 RAM 大小,网络接口信息,硬盘驱动信息,串行与并行信息),高级能源与资源配置、VMware 工具选项及能源管理选项。迁移虚拟机时,需要更改这个文件的格式到 KVM 支持的 xml 文件,并且重新确认文件信息的有效性。

    • .vmdk 文件:包含虚拟磁盘的信息。虚拟机的文件系统就建立在 .vmdk 文件上。迁移时需要拷贝这个文件到 KVM 主机,并且转换成能够被 libvirt 识别的文件格式。

    • 以下的虚拟机文件有些只在虚拟机处于的某种状态时出现,迁移过程中不会用到。例如当虚拟机开启时出现 .vswp 文件,当虚拟机暂停时出现 .vmss 文件。

    • .nvram 文件:包括虚拟机启动过程一部分的 Phoenix BIOS。它类似于拥有 BIOS 芯片的物理服务器,能够设置硬件配置选项。如果删除的话,在虚拟机启动时会自动地重新创建。

    • .vswp file:这些文件的大小等于分配给虚拟机的内存大小,再减去任何内存预留(默认是 0)。这些文件通常创建在虚拟机里,但是只有当 ESX 主机耗尽所有物理内存时才使用。当虚拟机关闭或暂停时,这些文件将删除。

    • .vmss 文件:这个文件用于虚拟机暂停时,保存虚拟机的存储内容,以便在重新开始时继续运行。

    • .vmsd 文件:这个文件与快照一起使用,用于存储元数据和其他活动在虚拟机里的每个快照的信息。这个文本文件在创建快照之前的初始大小是 0 字节,并在每次创建或删除快照时更新信息。

    • .vmsn 文件:这个文件与快照一起使用,用于存储虚拟机在进行快照时的状态。每在虚拟机上创建一个快照就会生成一个 .vmsn 文件,在删除快照时,文件自动删除。

    • .log 文件:这些文件创建来用于存储虚拟机的日志信息,并常常用于故障检查。在虚拟机目录里,有大量的这样的文件。当前的日志文件通常命名为 vmware.log。

    • .vmxf 文件:这是一个附加配置文件,不用于 ESX,用于与 Workstation 兼容的目的。这个文件是文本格式,Workstation 用来聚合虚拟机(VM teaming),将多个虚拟机分配成一组,作为一个单一对象开启或关闭、暂停或恢复它们。
      2.使用virt-v2v迁移虚拟机
      virt-v2v 是由 perl 语言编写的脚本,可以自动化的将创建在 Xen,KVM 和 VMware ESX 上的虚拟机拷贝到 virt-v2v 的主机,并且自动的更改配置,使之能够被 libvirt 进行管理。目前,virt-v2v 支持静态迁移下列虚拟机:RHEL4、RHEL5, RHEL6,Windows XP,Windows Vista, Windows 7, Windows Server 2003 和 Windows Server 2008。
      在这里插入图片描述
      迁移前的环境配置
      如果是要单纯的将虚拟机迁移到KVM的环境,则需要安装以下包。
      yum install -y qemu-kvm
      yum install -y libvirt
      yum install -y virt-manager
      yum install -y virt-v2v
      这里演示如何将VMspher下的linux虚拟机通过工具和VMware workstation手动的方式迁移到KVM环境中。
      使用virt-v2v工具自动迁移
      如果虚机是ESXi种的虚机,那么我们可以使用virt-v2v的工具直接进行自动迁移。virt-v2v工具可以将其他hypervisor平台上的虚拟机迁移到KVM上。它可以读取Xen,Hyper-v,VMware平台上的虚拟机,将它们迁移到由libvirt管理的KVM,openstack或者RHEV环境中。本节只针对ESCi上如何将虚机迁移到KVM中,往openstack和RHEV上迁移只需要转换镜像和导入即可不做赘述。
      KVM存储池配置过程
      mkdir -p /data/vmfs
      virsh pool-define-as vmdisk --type dir --target /data/vmfs
      virsh pool-build vmdisk
      virsh pool-autostart vmdisk
      virsh pool-start vmdisk
      创建ESXi认证文件
      (本步骤不是必须步骤,如果不做配置的话则在使用virt-v2v的时候需要稍微改变命令)
      vim /root/.netrc
      machine YourESXiServerIPAddress login root password YourESXiServerIPAddress
      chmod 0600 ~/.netrc
      开始迁移
      开始迁移前先测试看是否能够连接上ESXi主机
      virsh -c esx://172.17.20.80/no_verify=1 list --all
      1
      在这里插入图片描述
      如果能够看到运行在这个ESXi上的主机说明连接没有问题。
      开始迁移
      virt-v2v -ic esx://172.17.20.80/no_verify=1 -os vmdisk -of qcow2 -b virbr0 inner-db
      1
      注:1.如果遇到了类似于“ Peer certificate cannot be authenticated with given CA certificates”这一类的错误通过在url后面设置no_verify=1 参数来忽略证书检查。
      2.整个url的组成由user@esxi组成。如果在3.2没有创建认证文件的话这里需要将命令改成如下
      virt-v2v -ic esx://root@172.17.20.80/no_verify=1 -os vmdisk -of qcow2 -b virbr0 inner-db
      1
      3.如果用户名使用的是域账户(domain\name)或者虚机名中包含有反斜线,则反斜线需要转码特殊标示,使用“%5c”代替“\”
      4.-os表示指定到KVM中的哪个存储池,-of表示迁移转出的虚机磁盘格式,我这里指定的事qcow2,-b表示指定KVM中的虚拟网桥。
      输出结果如下
      [root@localhost ~]# virt-v2v -ic esx://172.17.20.80/?no_verify=1 -os vmdisk -of qcow2 -b virbr0 inner-db
      db_inner-db2: 38% [*]ETA 13:39db_inner-db2: 100% [=========]D 1h26m58s
      virt-v2v: No capability in config matches os=‘linux’ name=‘virtio’ distro=‘rhel’ major=‘3’ minor=‘8’
      virt-v2v: No capability in config matches os=‘linux’ name=‘cirrus’ distro=‘rhel’ major=‘3’ minor=‘8’
      virt-v2v: WARNING: Display driver was updated to cirrus, but unable to install cirrus driver. X may not function correctly
      virt-v2v: WARNING: /etc/fstab references unknown device /dev/cdrom. This entry must be manually fixed after conversion.
      virt-v2v: inner-db configured without virtio drivers.
      (因为迁移的目标虚机中没有安转virtio驱动所以报错,这里可以忽略)
      查看转换出来的xml配置文件
      在这里插入图片描述
      这里需要注意v2v工具把虚机的磁盘bus总重定义为了ide的,说明盘符变成了hd形式,如果出现开机无法加载则需要把启动步骤暂定进入启动文件修改启动分区,把/dev/sda1改成/dev/hda1,这里具体盘符视每个人的自己情况修改。
      测试虚机能否开启
      virsh list --all查看KVM下的虚机
      virsh strrt inner-db 开启虚拟机
      我这里就用图形化的截图来更加直观的显示结果。
      在这里插入图片描述
      手动迁移
      这里说的手动迁移思路指的是,将VMspher或者VMware workstation中的虚拟机运用命令的方式来迁移。在刚刚的VMware知识扩展中我们可以了解到,VMware存储虚机的文件系统可以分为两种模式,一种是单文件的存储,一种是多文件存储。在这里最好处理的就是单文件情况。如果遇到了多文件来提供虚机的磁盘存储情况我们需要使用vmware自带的工具来做文件合并。因为在KVM中是不支持多个存储文件的。介于在VMsphere中可以直接使用virt-v2v的命令来进行v2v的迁移,所以自动的就能将多存储文件合并,所以这里只针对在VMware workstation中如何将多存储文件进行合并。VMware Workstation自带了一个离线的磁盘管理工具vmware-vdiskmanager.exe。
      使用vmware-vdiskmanager.exe合并多个vmdk文件
      命令语法:
      vmware-vdiskmanager [选项]
      这里的选项你必须包含以下的一些选择项或参数
      选项和参数
      描述
      在powershell中运行vmware-vdiskmanager命令
      PS C:\WINDOWS\system32> F:\Program Files (x86)\VMware\VMware Workstation\vmware-vdiskmanager -r F:\Virtual Machines\db\i nner-db2.vmdk -t 0 F:\Virtual Machines\db\inner-db2-new.vmdk
      注意:
      1.指定vmware-vdiskmanager的绝对路径和与当前目录的相对路径
      2.多个文件存储的话指定那个几K或者几百K的vmdk描述文件而不用一一指定那些存储文件
      参数解释:
      -r
      转换已经指定类型的虚拟磁盘的类型,结果会输出创建一个新的虚拟磁盘。你必须用-t选项来指定你想要转换成的磁盘类型,并且指定目标虚拟磁盘的文件名。一旦转换完成,你可以先测试虚拟磁盘以确保它能够像你所希望的那样工作,然后再删除原来的那个虚拟磁盘文件。为了让虚拟机重新认识转换后的虚拟磁盘,你应该使用虚拟机设置编辑器先从虚拟机中移除先前存在的虚拟磁盘,然后添加转换好的虚拟磁盘给虚拟机。
      -t [0|1|2|3]
      你在创建一个新的虚拟磁盘或者重新配置一个虚拟磁盘时必须指定虚拟磁盘的类型。指定以下类型之一:
      0 —— 创建一个包含在单一虚拟文件中的可增长虚拟磁盘
      1 —— 创建一个被分割为每个文件2GB大小的可增长虚拟磁盘
      2 —— 创建一个包含在单一虚拟文件中的预分配虚拟磁盘
      3 —— 创建一个被分割为每个文件2GB大小的预分配虚拟磁盘
      vmware-vdiskmanager.exe使用说明
      Usage: vmware-vdiskmanager.exe OPTIONS |
      Offline disk manipulation utility
      Operations, only one may be specified at a time:
      -c : create disk. Additional creation options must
      be specified. Only local virtual disks can be
      created.
      -d : defragment the specified virtual disk. Only
      local virtual disks may be defragmented.
      -k : shrink the specified virtual disk. Only local
      virtual disks may be shrunk.
      -n : rename the specified virtual disk; need to
      specify destination disk-name. Only local virtual
      disks may be renamed.
      -p : prepare the mounted virtual disk specified by
      the mount point for shrinking.
      -r : convert the specified disk; need to specify
      destination disk-type. For local destination disks
      the disk type must be specified.
      -x : expand the disk to the specified capacity. Only
      local virtual disks may be expanded.
      -R : check a sparse virtual disk for consistency and attempt
      to repair any errors.
      -e : check for disk chain consistency.
      -D : make disk deletable. This should only be used on disks
      that have been copied from another product.
      Other Options:
      -q : do not log messages
      Additional options for create and convert:
      -a : (for use with -c only) adapter type
      (ide, buslogic, lsilogic). Pass lsilogic for other adapter types.
      -s : capacity of the virtual disk
      -t : disk type id
      Disk types:
      0 : single growable virtual disk
      1 : growable virtual disk split in 2GB files
      2 : preallocated virtual disk
      3 : preallocated virtual disk split in 2GB files
      4 : preallocated ESX-type virtual disk
      5 : compressed disk optimized for streaming
      6 : thin provisioned virtual disk - ESX 3.x and above
      The capacity can be specified in sectors, KB, MB or GB.
      The acceptable ranges:
      ide/scsi adapter : [1MB, 8192.0GB]
      buslogic adapter : [1MB, 2040.0GB]
      ex 1: vmware-vdiskmanager.exe -c -s 850MB -a ide -t 0 myIdeDisk.vmdk
      ex 2: vmware-vdiskmanager.exe -d myDisk.vmdk
      ex 3: vmware-vdiskmanager.exe -r sourceDisk.vmdk -t 0 destinationDisk.vmdk
      ex 4: vmware-vdiskmanager.exe -x 36GB myDisk.vmdk
      ex 5: vmware-vdiskmanager.exe -n sourceName.vmdk destinationName.vmdk
      ex 6: vmware-vdiskmanager.exe -r sourceDisk.vmdk -t 4 -h esx-name.mycompany.com
      -u username -f passwordfile “[storage1]/path/to/targetDisk.vmdk”
      ex 7: vmware-vdiskmanager.exe -k myDisk.vmdk
      ex 8: vmware-vdiskmanager.exe -p
      (A virtual disk first needs to be mounted at )
      转化成KVM内虚机
      转换成一个单文件的vmdk存储文件之后,将这个文件拷贝到你KVM环境下的linux机器中。
      qemu-img convert -f vmdk -O qcow2 inner-db.vmdk db_inner-db.qcow2
      (建议cd到你拷贝文件的目录执行,方便操作
      qemu-img info db_inner-db.qcow2
      [root@template test]# qemu-img info db_inner-db2
      image: db_inner-db2
      file format: qcow2
      virtual size: 50G (53687091200 bytes)
      disk size: 50G
      cluster_size: 65536
      在本地环境中挂载这个卷然后查看所要迁移虚机的磁盘格式,这个很重要,要是选择磁盘格式错误的话之后挂载很可能因为驱动问题无法找到根分区。这里给出三种挂载方式分别针对qcow2和raw的镜像来如何挂载。
      1.使用nbd挂载qcow2磁盘文件:(如果没有nbd驱动的话请看第二种)
      modprobe nbd max_part=8
      qemu-nbd -c /dev/nbd0 db_inner-db.qcow2
      mount /dev/nbd0p1 /mnt
      查看这个迁移虚机的分区。
      umount分区
      #umount /mnt
      #qemu-nbd -d /dev/nbd0
      2.当然你也可以使用guestmount来挂载
      guestmount -a 磁盘文件 –rw -m 要挂载的磁盘分区 本地目录
      guestmount -a db_inner-db2 --rw -m /dev/sda2 /mnt/
      1
      3.挂载raw格式的磁盘
      对于已分区的挂载:
      losetup /dev/loop0 db_inner-db2
      kpartx -a /dev/loop0
      mount /dev/mapper/loop0p1 /mnt
      注意:
      kpartx命令用来让Linux内核读取一个设备上的分区表,然后生成代表相应分区的设备。
      之后的话使用qemu命令来打开下这个镜像看是否能够打开
      qemu-kvm -name inner_db -smp 8 -m 8000 -drive file=db_inner-db2-flat.qcow2,if=virtio -net nic,model=virtio -net user -usbdevice tablet -vnc :94
      注意:如果待虚机的一开始没有安转virtio驱动的话,用这个命令是无法加载根分区的。
      然后使用vnc打开查看
      之后我在这里出了一个错误,错误如下
      在这里插入图片描述
      (笔者在之后用virt-v2v工具转的自后才发现虚机的磁盘驱动是ide的所以一开始很纳闷为什么会找不到根分区,因为笔者不知道虚机在vmware上配置。)
      如果虚机在vmware使用的是ide的bus总线则需要将virtio参数去掉。
      qemu-kvm -name inner_db -smp 8 -m 8000 -drive file=db_inner-db2-flat.qcow2 -net nic -net user -usbdevice tablet -vnc :94
      如果能够通过vnc链接看到系统正常加载启动的画面说明可以在KVM中使用这个镜像创建虚机。这个时候只需要使用virt-manager图形化操作即可。将虚机的镜像指定为本地的我们刚刚转好的qcow2镜像即可。
      在这里插入图片描述
      这里记得选择“import existing disk image”
      在这里插入图片描述
      在这里插入图片描述
      注意在之后的创建虚机的过程当中配置需要尽可能的与vmware中虚机的配置一致,否则很有可能导致创建不成功。
      后记
      如果有读者想在虚机中做这方面的测试一定要开启虚机支持硬件虚拟化,否则转换的时候会出现错误。
      类似于:qemu-img:…:CURL:Error opeing file: Server does not support ‘rang’(byte ranges)
      在这里插入图片描述
      查看是否支持硬件虚拟化的方法
      egrep ‘(vmx|svm)’ /proc/CPUinfo
      参考文献
      http://www.ibm.com/developerworks/cn/linux/l-cn-mgrtvm3/
      http://libguestfs.org/virt-v2v.1.html#output-to-rhev
      操作说明
      现有一台kvm宿主机10.10.200.227,上面有通过libvirt管理的多台vm,需要将上面的一台Centos 7 guest主机迁移至ovirt虚拟化平台中。
      ovirt环境说明:
      ovirt-engine:10.10.200.229
      ovirt-host:10.10.200.228

        ovirt-storage:10.10.200.43,这里使用的nfs存储,export存储路径为10.10.200.43:/home/nfs
      

    操作步骤
    1.配置ovirt的export存储域
    在ovirt-engine中新建export的存储域
    在这里插入图片描述
    2.查看kvm宿主主机中的vm,并选择一台vm进行迁移
    [html] view plain copy
    [root@ovirt-host-227 ~]# virsh list --all
    Id Name State

    2 windows2008 running

    • Centos7                        shut off  
      
    • Centos7-2                      shut off  
      
    • windows2003                    shut off  
      
    • windows2008-1                  shut off  
      
    • windows2008-vmware             shut off  
      

    在这里我们选择Centos7这台vm进行迁移,在迁移需要在宿主服务器上安装virt-v2v

    [html] view plain copy
    #yum -y install virt-v2v
    下面我们通过virt-v2v进行迁移Centos7这台vm,-os参数后面的存储路径为前面配置的export存储域的存储路径,–network的参数为vnet,ovirt中的vnet名称一般都为ovirtmgmt

    [html] view plain copy
    [root@ovirt-host-227 ~]# virt-v2v -i libvirt -o rhev -os 10.10.200.43:/home/nfs --network ovirtmgmt Centos7
    [ 0.0] Opening the source -i libvirt Centos7
    [ 0.0] Creating an overlay to protect the source from being modified
    [ 0.0] Opening the overlay
    [ 5.0] Initializing the target -o rhev -os 10.10.200.43:/home/nfs
    [ 5.0] Inspecting the overlay
    [ 18.0] Checking for sufficient free disk space in the guest
    [ 18.0] Estimating space required on target for each disk
    [ 18.0] Converting CentOS Linux release 7.2.1511 (Core) to run on KVM
    virt-v2v: This guest has virtio drivers installed.
    [ 56.0] Mapping filesystem data to avoid copying unused and blank areas
    [ 58.0] Closing the overlay
    [ 58.0] Checking if the guest needs BIOS or UEFI to boot
    [ 58.0] Copying disk 1/1 to /tmp/v2v.jXKwCN/3dfea236-3b8a-4da8-b8bf-fae9768045e4/images/c271532e-d60e-41f1-b577-bf7f0cdf9533/819dbc8a-37c7-432e-a36f-539d39bf879e (qcow2)
    (100.00/100%)
    [ 104.0] Creating output metadata
    [ 104.0] Finishing off
    3,数据迁移完成后,我们在ovirt-engine中将vm添加至ovirt中

    在这里插入图片描述
    在这里插入图片描述
    在添加完成后,可以在ovirt-engine的虚拟机这一栏,看见Centos7名称的vm,
    在这里插入图片描述
    在刚刚添加的时候,vm处于import状态,需要等一段时间,待日志提示已经imported success后,我们可以启动该vm,第一次启动相对耗时较长,待vm启动后,可以查看的为
    up状态
    在这里插入图片描述
    virt-v2v 使用指南
    1.定义.
    virt-v2v将外部的虚拟化平台上的虚拟机转化到可以运行的KVM平台上。它可以读取在VMware、Xen运行Hyper-V和其他虚拟机管理程序上的Windows和Linux的虚拟机,并将其转换为KVM的libvirt,OpenStack,oVirt管理,红帽虚拟化(RHV)等几种方式。
    2.基本转化图
    [html] view plain copy
    ┌────────────┐ ┌─────────▶ -o null
    -i disk ────────────┐ │ │ ─┘┌───────▶ -o local
    -i ova ──────────┐ └──▶ │ virt-v2v │ ──┘┌───────▶ -o qemu
    └────▶ │ conversion │ ───┘┌────────────┐
    VMware─▶┌────────────┐ │ server │ ────▶ -o libvirt │─▶ KVM
    Xen ───▶│ -i libvirt ──▶ │ │ │ (default) │
    … ───▶│ (default) │ │ │ ──┐ └────────────┘
    └────────────┘ │ │ ─┐└──────▶ -o glance
    -i libvirtxml ─────────▶ │ │ ┐└─────────▶ -o rhv
    └────────────┘ └──────────▶ -o vdsm
    3.安装
    (主机是centos7.2系统)
    3.1 KVM 需要有 CPU 的支持(Intel vmx或 AMD svm),在安装 KVM 之前检查一下 CPU 是否提供了虚拟技术的支持:

    egrep’^flags.*(vmx|svm)’ /proc/cpuinfo

    有显示, 有显示则说明处理器具有VT功能,在主板BIOS中开启CPU的Virtual Technolege(VT,虚化技术)
    3.2 安装kvm及其需要的软件包

    yum installkvm kmod-kvm qemu kvm-qemu-img virt-viewer virt-manager libvirt libvirt-pythonpython-virtinst

    或# yum groupinstall KVM
    检查kvm模块是否安装,使用以下命令显示两个模块则表示安装完成

    lsmod | grep kvm

    3.3 安装irt-v2v
    #yum install virt-v2v
    4.基本参数语法
    -i libvirt 读取本地libvirt的资源。当然libvirt 可以连接多中虚拟化平台的资源,比如Vmware,Xen,或者其他的。当连接外部的虚拟化平台时使用 -ic 。
    -i libvirtxml 读取libvirt 的XML文件。
    -i vmx 是直接读取vmware的image的文件
    -i ova 是读取VMware的ova 文件。
    -o glance 是输出到 OpenStack Glance.
    -o libvirt 可以输出到任意的libvirt 目标,可以连接本地的和远端的kvm虚拟化平台。使用-oc 到远端。
    -o local 输出到本地的磁盘镜像和一个配置文件。(一般也不用)
    -o qemu 和local比较像,一般也不用。
    -o rhv 输出到rhv平台上。
    -o vdsm 只是用来当 virt-v2v 在VDSM的运行控制下(基本不用).
    –in-place instructs virt-v2v to customize the guest OS in the input virtual machine, instead of creating a new VM in the target hypervisor.
    5.支持的平台和虚拟机类型。
    VMware ESXi >=5.0
    OVA exported from VMware
    OVAs from other hypervisors will not work.
    SUSE Xen
    Citrix Xen
    Citrix Xen has not been recently tested.
    Hyper-V(不一定好使)
    从本地但是这个images必须支持虚拟化,并且格式是qemu支持的。
    物理机器:
    使用 virt-p2v
    Virtualization management systems (Output)
    OpenStack Glance
    Red Hat Virtualization (RHV) 4.1 and up
    Local libvirt
    And hence virsh(1), virt-manager(1), and similar tools.
    Local disk
    Red Hat Enterprise Linux 3, 4, 5, 6, 7
    CentOS 3, 4, 5, 6, 7
    Scientific Linux 3, 4, 5, 6, 7
    Oracle Linux
    Fedora
    SLES 10 and up
    OpenSUSE 10 and up
    Windows XP to Windows 10
    Currently NT 5.2 to NT 6.3 are supported.
    虚拟机固件
    BIOS or UEFI for all guest types
    6.简单操作的实例。
    1:从VMware vcenter server 到本地libvirt。
    首先需要有一个VMware vcenter server 然后设置好Datacenter。
    virt-v2v -ic vpx://vcenter.example.com/Datacenter/esxi vmware_guest
    转换后是在本地/var/lib/libvirt/images
    2:从VMware vcenter server 到RHV/ovirt.
    首先确保Vmware 和 RHV 平台配置运行正常。(关于RHV的有其他章节介绍)
    virt-v2v -ic vpx://vcenter.example.com/Datacenter/esxi vmware_guest
    -o rhv -os rhv.nfs:/export_domain --network rhvm
    3:从本地到OpenStack glance。
    (现在仅仅支持从kvm本地到openstack)
    virt-v2v -i disk disk.img -o glance
    4:从本地到本地。(比较简单,实现方式也比较多,注意改名字)
    virt-v2v -i disk disk.img -o local -os /var/tmp
    virt-v2v -i libvirtxml guest-domain.xml -o local -os /var/tmp
    virt-v2v -i disk disk.img -o qemu -os /var/tmp --qemu-boot
    virt-v2v命令解析:
    lvirt-v2v -ic esx://128.9.67.30/?no_verify=1 -os migration -ofqcow2 --bridge virbr0 test_v2v
    p-icesx://128.9.67.30/?no_verify=1 : 输入URI,提供Vmware所在单板地址,?no_verify=1,忽略SSL证书检查。
    p-os : 输出目标机器上存储域名字。
    p-of:转换后KVM上虚拟机镜像格式
    p–bridge:网桥名,虚拟机在迁移之后将连接KVM 主机网络, 因此主机必须有一个与之相匹配的网络接口
    ptest_v2v:Vmware上源虚拟机名字,配合URI使用

    一:命令行参数解析
    l源码再现:
    lGetOptions(“ic=s” => $input_uri,
    l “os=s” =>$output_storage,
    l “op=s” => $output_storage, # Deprecated
    l “of=s” => $output_format, ……)
    l功能:
    l接收命令行输入的参数,并移出散列@ARGV;
    l
    二:配置文件的加载
    l源码再现:
    lpush(@config_files, ‘/etc/virt-v2v.conf’) if -r’/etc/virt-v2v.conf’;
    lpush(@config_files, ‘/var/lib/virt-v2v/virt-v2v.db’)
    l if -r ‘/var/lib/virt-v2v/virt-v2v.db’;
    l功能:
    l安装virt-v2v后KVM机器上自动生成两个配置文件,分别是virt-v2v.conf和virt-v2v.db,记录执行virt-v2v需要哪些配置或者依赖。
    三:读取配置文件
    l源码再现:
    lpush(@config_files, ‘/etc/virt-v2v.conf’) if -r’/etc/virt-v2v.conf’;
    lpush(@config_files, ‘/var/lib/virt-v2v/virt-v2v.db’)
    l if -r ‘/var/lib/virt-v2v/virt-v2v.db’;
    l功能:
    l安装virt-v2v后KVM机器上自动生成两个配置文件,分别是virt-v2v.conf和virt-v2v.db,记录执行virt-v2v需要哪些配置或者依赖。
    四:登录

    展开全文
  • ConcurrentHashMap & HashTable的知识点都整理了一下

    万次阅读 多人点赞 2019-12-17 22:32:14
    居然不同点几乎都说出来了,被人遗忘的Hashtable都能说得头头是道,看来不简单,不知道接下来的ConcurrentHashMap连环炮能不能顶得住了。 都说了他的并发度不够,性能很低,这个时候你都怎么处理的? 他...

    你知道的越多,你不知道的越多

    点赞再看,养成习惯

    本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

    前言

    作为一个在互联网公司面一次拿一次Offer的面霸,打败了无数竞争对手,每次都只能看到无数落寞的身影失望的离开,略感愧疚(请允许我使用一下夸张的修辞手法)。

    于是在一个寂寞难耐的夜晚,我痛定思痛,决定开始写互联网技术栈面试相关的文章,希望能帮助各位读者以后面试势如破竹,对面试官进行360°的反击,吊打问你的面试官,让一同面试的同僚瞠目结舌,疯狂收割大厂Offer!

    所有文章的名字只是我的噱头,我们应该有一颗谦逊的心,所以希望大家怀着空杯心态好好学,一起进步。

    回手掏

    上次面试呀,我发现面试官对我的几个回答还是不够满意,觉得还是有点疑问,我就挑几个回答一下。

    16是2的幂,8也是,32也是,为啥偏偏选了16?

    我觉得就是一个经验值,定义16没有很特别的原因,只要是2次幂,其实用 8 和 32 都差不多。

    用16只是因为作者认为16这个初始容量是能符合常用而已。

    Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表,为啥呢?

    根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

    正文

    一个婀娜多姿,穿着衬衣的小姐姐,拿着一个精致的小笔记本,径直走过来坐在我的面前。

    就在我口水要都要流出来的时候,小姐姐的话语打断了我的YY。

    喂小鬼,你养我啊!

    呸呸呸,说错了,上次的HashMap回答得不错,最后因为天色太晚了面试草草收场,这次可得好好安排你。

    诶,面试官上次是在抱歉,因为公司双十二要值班,实在是没办法,不过这次不会了,我推掉了所有的事情准备全身心投入到今天的面试中,甚至推掉了隔壁王大爷的约会邀约。

    这样最好,上次我们最后聊到HashMap在多线程环境下存在线程安全问题,那你一般都是怎么处理这种情况的?

    美丽迷人的面试官您好,一般在多线程的场景,我都会使用好几种不同的方式去代替:

    • 使用Collections.synchronizedMap(Map)创建线程安全的map集合;
    • Hashtable
    • ConcurrentHashMap

    不过出于线程并发度的原因,我都会舍弃前两者使用最后的ConcurrentHashMap,他的性能和效率明显高于前两者。

    哦,Collections.synchronizedMap是怎么实现线程安全的你有了解过么?

    卧*!不按照套路出牌呀,正常不都是问HashMap和ConcurrentHashMap么,这次怎么问了这个鬼东西,还好我饱读诗书,经常看敖丙的《吊打面试官》系列,不然真的完了。

    小姐姐您这个问题真好,别的面试官都没问过,说真的您水平肯定是顶级技术专家吧。

    别贫嘴,快回答我的问题!抿嘴一笑😁

    在SynchronizedMap内部维护了一个普通对象Map,还有排斥锁mutex,如图

    Collections.synchronizedMap(new HashMap<>(16));

    我们在调用这个方法的时候就需要传入一个Map,可以看到有两个构造器,如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象。

    如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象,就是上面的Map。

    创建出synchronizedMap之后,再操作map的时候,就会对方法上锁,如图全是🔐

    卧*,小伙子,秒啊,其实我早就忘了源码了,就是瞎问一下,没想到还是回答上来了,接下来就面对疾风吧。

    回答得不错,能跟我聊一下Hashtable么?

    这个我就等着你问呢嘿嘿!

    跟HashMap相比Hashtable是线程安全的,适合在多线程的情况下使用,但是效率可不太乐观。

    哦,你能说说他效率低的原因么?

    嗯嗯面试官,我看过他的源码,他在对数据操作的时候都会上锁,所以效率比较低下。

    除了这个你还能说出一些Hashtable 跟HashMap不一样点么?

    !呐呢?这叫什么问题嘛?这个又是知识盲区呀!

    呃,面试官我从来没使用过他,你容我想想区别的点,说完便开始抓头发,这次不是装的,是真的!

    Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null。

    呃我能打断你一下么?为啥 Hashtable 是不允许 KEY 和 VALUE 为 null, 而 HashMap 则可以呢?

    尼*,我这个时候怎么觉得面前的人不好看了,甚至像个魔鬼,看着对自己面试官心里想到。

    因为Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    但是你还是没说为啥Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null?

    这是因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。

    如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。

    好的你继续说不同点吧。

    • 实现方式不同:Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。

      Dictionary 是 JDK 1.0 添加的,貌似没人用过这个,我也没用过。

    • 初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。

    • 扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。

    • 迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。

      所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。

    fail-fast是啥?

    卧*,你自己不知道么?为啥问我!!!还好我会!

    快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

    他的原理是啥?

    迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。

    集合在被遍历期间如果内容发生变化,就会改变modCount的值。

    每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

    Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。

    因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

    说说他的场景?

    java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)算是一种安全机制吧。

    Tip安全失败(fail—safe)大家也可以了解下,java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

    嗯?这个小鬼这么有东西的嘛?居然把不同点几乎都说出来了,被人遗忘的Hashtable都能说得头头是道,看来不简单,不知道接下来的ConcurrentHashMap连环炮能不能顶得住了。

    都说了他的并发度不够,性能很低,这个时候你都怎么处理的?

    他来了他来了,他终于还是来了,等了这么久,就是等你问我这个点,你还是掉入了我的陷阱啊,我早有准备,在HashMap埋下他线程不安全的种子,就是为了在ConcurrentHashMap开花结果!

    小姐姐:这样的场景,我们在开发过程中都是使用ConcurrentHashMap,他的并发的相比前两者好很多。

    哦?那你跟我说说他的数据结构吧,以及为啥他并发度这么高?

    ConcurrentHashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

    我先说一下他在1.7中的数据结构吧:

    如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表

    Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

    static final class Segment<K,Vextends ReentrantLock implements Serializable {

        private static final long serialVersionUID = 2249069246763182397L;

        // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
        transient volatile HashEntry<K,V>[] table;

        transient int count;
            // 记得快速失败(fail—fast)么?
        transient int modCount;
            // 大小
        transient int threshold;
            // 负载因子
        final float loadFactor;

    }

    HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。

    volatile的特性是啥?

    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性

    • 禁止进行指令重排序。(实现有序性

    • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性

    我就不大篇幅介绍了,多线程章节我会说到的,大家知道用了之后安全了就对了。

    那你能说说他并发度高的原因么?

    原理上来说,ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。

    不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。

    每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

    就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();//这就是为啥他不可以put null值的原因
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          
             (segments, (j << SSHIFT) + SBASE)) == null
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

    他先定位到Segment,然后再进行put操作。

    我们看看他的put源代码,你就知道他是怎么做到线程安全的了,关键句子我注释了。

            final V put(K key, int hash, V value, boolean onlyIfAbsent) {
              // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
                HashEntry<K,V> node = tryLock() ? null :
                    scanAndLockForPut(key, hash, value);
                V oldValue;
                try {
                    HashEntry<K,V>[] tab = table;
                    int index = (tab.length - 1) & hash;
                    HashEntry<K,V> first = entryAt(tab, index);
                    for (HashEntry<K,V> e = first;;) {
                        if (e != null) {
                            K k;
     // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
                            if ((k = e.key) == key ||
                                (e.hash == hash && key.equals(k))) {
                                oldValue = e.value;
                                if (!onlyIfAbsent) {
                                    e.value = value;
                                    ++modCount;
                                }
                                break;
                            }
                            e = e.next;
                        }
                        else {
                     // 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
                            if (node != null)
                                node.setNext(first);
                            else
                                node = new HashEntry<K,V>(hash, key, value, first);
                            int c = count + 1;
                            if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                                rehash(node);
                            else
                                setEntryAt(tab, index, node);
                            ++modCount;
                            count = c;
                            oldValue = null;
                            break;
                        }
                    }
                } finally {
                   //释放锁
                    unlock();
                }
                return oldValue;
            }

    首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

    1. 尝试自旋获取锁。
    2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

    那他get的逻辑呢?

    get 逻辑比较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。

    由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

    ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁

    你有没有发现1.7虽然可以支持每个Segment并发访问,但是还是存在一些问题?

    是的,因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样问题,所以他在jdk1.8完全优化了。

    那你再跟我聊聊jdk1.8他的数据结构是怎么样子的呢?

    其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

    跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。

    同样的,你能跟我聊一下他值的存取操作么?以及是怎么保证线程安全的?

    ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:

    1. 根据 key 计算出 hashcode 。
    2. 判断是否需要进行初始化。
    3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
    4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
    5. 如果都不满足,则利用 synchronized 锁写入数据。
    6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

    你在上面提到CAS是什么?自旋又是什么?

    CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的。

    CAS 操作的流程如下图所示,线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

    这是一种乐观策略,认为并发操作并不总会发生。

    还是不明白?那我再说明下,乐观锁在实际开发场景中非常常见,大家还是要去理解。

    就比如我现在要修改数据库的一条数据,修改之前我先拿到他原来的值,然后在SQL里面还会加个判断,原来的值和我手上拿到的他的原来的值是否一样,一样我们就可以去修改了,不一样就证明被别的线程修改了你就return错误就好了。

    SQL伪代码大概如下:

    update a set value = newValue where value = #{oldValue}//oldValue就是我们执行前查询出来的值 

    CAS就一定能保证数据没被别的线程修改过么?

    并不是的,比如很经典的ABA问题,CAS就无法判断了。

    什么是ABA?

    就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。

    但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。

    那怎么解决ABA问题?

    用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。

    update a set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} // 判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不一样

    牛*,有点东西,除了版本号还有别的方法保证么?

    其实有很多方式,比如时间戳也可以,查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间,这样也能保证,方法很多但是跟版本号都是异曲同工之妙,看场景大家想怎么设计吧。

    CAS性能很高,但是我知道synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?

    synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。

    针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁

    所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。

    🐂,那我们回归正题,ConcurrentHashMap的get操作又是怎么样子的呢?

    • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
    • 如果是红黑树那就按照树的方式获取值。
    • 就不满足那就按照链表的方式遍历获取值。

    小结:1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

    总结

    Hashtable&ConcurrentHashMap跟HashMap基本上就是一套连环组合,我在面试的时候经常能吹上很久,经常被面试官说:好了好了,我们继续下一个话题吧哈哈。

    是的因为提到HashMap你肯定会聊到他的线程安全性这一点,那你总不能加锁一句话就搞定了吧,java的作者们也不想,所以人家写开发了对应的替代品,那就是线程安全的Hashtable&ConcurrentHashMap。

    两者都有特点,但是线程安全场景还是后者用得多一点,原因我在文中已经大篇幅全方位的介绍了,这里就不再过多赘述了。

    你们发现了面试就是一个个的坑,你说到啥面试官可能就怼到你啥,别问我为啥知道嘿嘿。

    你知道不确定能不能为这场面试加分,但是不知道肯定是减分的,文中的快速失败(fail—fast)问到,那对应的安全失败(fail—safe)也是有可能知道的,我想读者很多都不知道吧,因为我问过很多仔哈哈。

    还有提到CAS乐观锁,你要知道ABA,你要知道解决方案,因为在实际的开发场景真的不要太常用了,sync的锁升级你也要知道。

    我没过多描述线程安全的太多东西,因为我都写了,以后更啥?对吧哈哈。

    常见问题

    • 谈谈你理解的 Hashtable,讲讲其中的 get put 过程。ConcurrentHashMap同问。
    • 1.8 做了什么优化?
    • 线程安全怎么做的?
    • 不安全会导致哪些问题?
    • 如何解决?有没有线程安全的并发容器?
    • ConcurrentHashMap 是如何实现的?
    • ConcurrentHashMap并发度为啥好这么多?
    • 1.7、1.8 实现有何不同?为什么这么做?
    • CAS是啥?
    • ABA是啥?场景有哪些,怎么解决?
    • synchronized底层原理是啥?
    • synchronized锁升级策略
    • 快速失败(fail—fast)是啥,应用场景有哪些?安全失败(fail—safe)同问。
    • ……

    加分项

    在回答Hashtable和ConcurrentHashMap相关的面试题的时候,一定要知道他们是怎么保证线程安全的,那线程不安全一般都是发生在存取的过程中的,那get、put你肯定要知道。

    HashMap是必问的那种,这两个经常会作为替补问题,不过也经常问,他们本身的机制其实都比较简单,特别是ConcurrentHashMap跟HashMap是很像的,只是是否线程安全这点不同。

    提到线程安全那你就要知道相关的知识点了,比如说到CAS你一定要知道ABA的问题,提到synchronized那你要知道他的原理,他锁对象,方法、代码块,在底层是怎么实现的。

    synchronized你还需要知道他的锁升级机制,以及他的兄弟ReentantLock,两者一个是jvm层面的一个是jdk层面的,还是有很大的区别的。

    那提到他们两个你是不是又需要知道juc这个包下面的所有的常用类,以及他们的底层原理了?

    那提到……

    白嫖不好,创作不易,各位的点赞就是丙丙创作的最大动力,我们下篇文章见,文末图片有福利

    持续更新,未完待续……


    文章每周持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读,回复【资料】【面试】有我准备的一线大厂面试资料和文章,本文 GitHub https://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

    你知道的越多,你不知道的越多

    展开全文
  • 请思考 结: 三、Manacher原理 假设遍历到位置i,如何操作呢 四、代码及复杂度分析 前缀树 后缀树/后缀数组 后缀树:后缀树,就是一串字符的所有后缀保存并且压缩的字典树。 相对于字典树来说,后缀树并不是针对...

    本文代码实现基本按照《数据结构》课本目录顺序,外加大量的复杂算法实现,一篇文章足够。能换你一个收藏了吧?

     当然如果落下什么了欢迎大家评论指出

    目录

    顺序存储线性表实现 

    单链表不带头标准c语言实现

    单链表不带头压缩c语言实现

    约瑟夫环-(数组、循环链表、数学) 

    线性表表示集合

     线性表实现一元多项式操作

    链表环问题

     

    移除链表元素

    回文链表

    链表表示整数,相加

    LRU

    LFU

    合并链表

    反转链表

     反转链表2

    对链表排序

    旋转链表

     数组实现栈

    链表实现栈

    数组实现队列

    链表实现队列

    双栈的实现

     栈/队列 互相模拟实现

    栈的排序

    栈——括号匹配

    栈——表达式求值 

    借汉诺塔理解栈与递归

    单调栈

    双端单调队列

     单调队列优化的背包问题

    01背包问题 

    完全背包问题 

    多重背包问题 

     串的定长表示

    串的堆分配实现

    KMP

    一、引子

    二、分析总结

    三、基本操作

    四、原理

    五、复杂度分析

    Manacher

    小问题一:请问,子串和子序列一样么?请思考一下再往下看

    小问题二:长度为n的字符串有多少个子串?多少个子序列?

    一、分析枚举的效率

    二、初步优化

    问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)

    那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考

    小结:

    三、Manacher原理

    假设遍历到位置i,如何操作呢

    四、代码及复杂度分析

    前缀树

    后缀树/后缀数组

    后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。

     

    相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。

    后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)

    AC自动机

    数组缺失

    二叉树遍历

    前序

    中序

    后序

    进一步思考

    二叉树序列化/反序列化

    先序中序后序两两结合重建二叉树

    先序遍历

    中序遍历

    后序遍历

    层次遍历

    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

    输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

    输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

    先序中序数组推后序数组

    二叉树遍历

    遍历命名

    方法1:我们可以重建整棵树:

    https://blog.csdn.net/hebtu666/article/details/84322113

    方法2:我们可以不用重建,直接得出:

    根据数组建立平衡二叉搜索树

    java整体打印二叉树

    判断平衡二叉树

    判断完全二叉树

    判断二叉搜索树

    二叉搜索树实现

    堆的简单实现

    堆应用例题三连

    一个数据流中,随时可以取得中位数。

    金条

    项目最大收益(贪心问题)

     并查集实现

    并查集入门三连:HDU1213 POJ1611 POJ2236

    HDU1213

    POJ1611

     POJ2236

    线段树简单实现

    功能:一样的,依旧是查询和改值。

    查询[s,t]之间最小的数。修改某个值。

    那我们继续说,如何查询。

    如何更新?

     树状数组实现

    最大搜索子树

    morris遍历

    最小生成树

    拓扑排序

    最短路

     

    简单迷宫问题

    深搜DFS\广搜BFS 

     皇后问题

    一般思路:

    优化1:

    优化2:

    二叉搜索树实现

    Abstract Self-Balancing Binary Search Tree

     

    二叉搜索树

    概念引入

    AVL树

    红黑树

    size balance tree

    伸展树

    Treap

    最简单的旋转

    带子树旋转

    代码实现

    AVL Tree

    前言

    二叉搜索树

    AVL Tree

    旋转

    旋转总结

    单向右旋平衡处理LL:

    单向左旋平衡处理RR:

    双向旋转(先左后右)平衡处理LR:

    双向旋转(先右后左)平衡处理RL:

    深度的记录

    单个节点的深度更新

    写出旋转代码

    总写调整方法

    插入完工

    删除

    直观表现程序

    跳表介绍和实现

    c语言实现排序和查找所有算法

     

     


    顺序存储线性表实现 

    在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。

     

    顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。

    优点:随机存取表中元素。缺点:插入和删除操作需要移动元素。

     

    线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。

    给出两种基本实现:

    /*
    静态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INITSIZE 100
    #define ElemType int
    #define Status int
    #define OK     1
    #define ERROR  0
    
    typedef struct
    {
    	ElemType elem[LIST_INITSIZE];
    	int length;
    }SqList;
    
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DisCreat(SqList A,SqList *B,SqList *C);//拆分(按正负),也可以根据需求改
    //虽然思想略简单,但是要写的没有错误,还是需要锻炼coding能力的
    
    Status InitList(SqList *L)
    {
        L->length = 0;//长度为0
        return OK;
    }
    
    Status ListInsert(SqList *L, int i,ElemType e)
    {
        int j;
        if(i<1 || i>L->length+1)
            return ERROR;//判断非法输入
        if(L->length == LIST_INITSIZE)//判满
        {
            printf("表已满");//提示
            return ERROR;//返回失败
        }
        for(j = L->length;j > i-1;j--)//从后往前覆盖,注意i是从1开始
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        (L->length)++;//表长加1
        return OK;//反回成功
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i = 0;i < L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DisCreat(SqList A,SqList *B,SqList *C)
    {
        int i;
        for(i = 0;i < A.length;i++)//依次遍历A中元素
        {
            if(A.elem[i]<0)//判断
                ListInsert(B,B->length+1,A.elem[i]);//直接调用插入函数实现尾插
            else
                ListInsert(C,C->length+1,A.elem[i]);
        }
    }
    
    int main(void)
    {
        //复制的
    	SqList L;
    	SqList B, C;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,33,-3,-88,21,77,0,-9};
    	InitList(&L);
    	InitList(&B);
    	InitList(&C);
    	for (i = 1; i <= 9; i++)
    		ListInsert(&L,i,data[i-1]);
        printf("插入完成后L = : ");
    	ListPrint(L);
        ListDelete(&L,1,&e);
    	printf("删除第1个后L = : ");
    	ListPrint(L);
        DisCreat(L , &B, &C);
    	printf("拆分L后B = : ");
    	ListPrint(B);
    	printf("拆分L后C = : ");
    	ListPrint(C);
    	printf("拆分L后L = : ");
    	ListPrint(L);
    }

    静态:长度固定

    动态:不够存放可以加空间(搬家)

     

    /*
    子任务名任务:1_2 动态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INIT_SIZE 100
    #define LISTINCREMENT 10
    #define Status int
    #define OVERFLOW -1
    #define OK 1
    #define ERROR 0
    #define ElemType int
    
    typedef struct
    {
    	ElemType * elem;
    	int length;
    	int listsize;
    }SqList;
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DeleteMin(SqList *L);//删除最小
    
    Status InitList(SqList *L)
    {
        L->elem = (ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));//申请100空间
    	if(!L->elem)//申请失败
    		return ERROR;
    	L->length = 0;//长度0
    	L->listsize = LIST_INIT_SIZE;//容量100
    	return OK;//申请成功
    }
    
    Status ListInsert(SqList *L,int i,ElemType e)
    {
        int j;
        ElemType *newbase;
        if(i<1 || i>L->length+1)
            return ERROR;//非法输入
            
        if(L->length >= L->listsize)//存满了,需要更大空间
        {
            newbase = (ElemType*)realloc(L->elem,(L->listsize+LISTINCREMENT)*sizeof(ElemType));//大10的空间
            if(!newbase)//申请失败
                return ERROR;
            L->elem = newbase;//调指针
            L->listsize+= LISTINCREMENT;//新容量
        }
        
        for(j=L->length;j>i-1;j--)//从后往前覆盖
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        L->length++;//长度+1
        return OK;
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i=0;i<L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DeleteMin(SqList *L)
    {
        //表空在Listdelete函数里判断
        int i;
        int j=0;//最小值下标
        ElemType *e;
        for(i=0;i<L->length;i++)//寻找最小
        {
            if(L->elem[i] < L->elem[j])
                j=i;
        }
        ListDelete(L,j+1,&e);//调用删除,注意j要+1
    }
    
    int main(void)
    {
    	SqList L;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,-33,3,-88,21,77,0,-9};
    	InitList(&L);
    	for (i = 1; i <= 9; i++)
    	{
    		ListInsert(&L,i,data[i-1]);
    	}
    	printf("插入完成后 L = : ");
    	ListPrint(L);
        ListDelete(&L, 2, &e);
    	printf("删除第 2 个后L = : ");
    	ListPrint(L);
        DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    }

    单链表不带头标准c语言实现

     

    链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

    使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表双向链表以及循环链表

     

    下面给出不带头的单链表标准实现:

    定义节点:

    typedef struct node 
    { 
        int data;
        struct node * next;
    }Node;

    尾插:

    void pushBackList(Node ** list, int data) 
    { 
        Node * head = *list;
        Node * newNode = (Node *)malloc(sizeof(Node));//申请空间
        newNode->data = data; newNode->next = NULL;
        if(*list == NULL)//为空
            *list = newNode;
        else//非空
        {
            while(head ->next != NULL)
                head = head->next;
            head->next = newNode;
        }
    }
    
    

    插入:

    int insertList(Node ** list, int index, int data) 
    {
        int n;
        int size = sizeList(*list); 
        Node * head = *list; 
        Node * newNode, * temp;
        if(index<0 || index>size) return 0;//非法
        newNode = (Node *)malloc(sizeof(Node)); //创建新节点
        newNode->data = data; 
        newNode->next = NULL;
        if(index == 0) //头插
        {
            newNode->next = head; 
            *list = newNode; 
            return 1; 
        }
        for(n=1; n<index; n++) //非头插
            head = head->next;
        if(index != size) 
            newNode->next = head->next; 
        //链表尾部next不需指定
        head->next = newNode; 
        return 1;
    }
    

    按值删除:

    void deleteList(Node ** list, int data) 
    { 
        Node * head = *list; Node * temp; 
        while(head->next!=NULL) 
        { 
            if(head->next->data != data) 
            { 
                head=head->next; 
                continue; 
            } 
            temp = head->next;
            if(head->next->next == NULL) //尾节点删除
                head->next = NULL; 
            else 
                head->next = temp->next; 
            free(temp);
        }    
        head = *list; 
        if(head->data == data) //头结点删除
        { 
            temp = head; 
            *list = head->next; 
            head = head->next; 
            free(temp); 
        }
    }
    

    打印:

    void printList(Node * head) 
    { 
        Node * temp = head; 
        for(; temp != NULL; temp=temp->next) 
            printf("%d ", temp->data); 
        printf("\n"); 
    }

    清空:

    void freeList(Node ** list) 
    { 
        Node * head = *list; 
        Node * temp = NULL; 
        while(head != NULL) //依次释放
        { 
            temp = head; 
            head = head->next; 
            free(temp); 
        } 
        *list = NULL; //置空
    }

    别的也没啥了,都是基本操作

    有些代码要分情况,很麻烦,可读性较强吧

     

    单链表不带头压缩c语言实现

     

     

     注:单追求代码简洁,所以写法可能有点不标准。

    //第一次拿c开始写数据结构,因为自己写的,追求代码量少,和学院ppt不太一样。有错请指出
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    typedef struct node//定义节点
    {
        int data;
        struct node * next;
    }Node;

     

    //函数介绍
    void printlist(Node * head)//打印链表
    int lenlist(Node * head)//返回链表长度
    void insertlist(Node ** list,int data,int index)//插入元素
    void pushback(Node ** head,int data)//尾部插入
    void freelist(Node ** head)//清空链表
    void deletelist(Node ** list,int data)//删除元素
    Node * findnode(Node ** list,int data)//查找
    void change(Node ** list,int data,int temp)//改变值

    打印

    void printlist(Node * head)//打印链表
    {
        for(;head!=NULL;head=head->next) printf("%d ",head->data);
        printf("\n");//为了其他函数打印,最后换行
    }

    链表长度

    int lenlist(Node * head)//返回链表长度
    {
        int len;
        Node * temp = head;
        for(len=0; temp!=NULL; len++) temp=temp->next;
        return len;
    }

    插入元素

    void insertlist(Node ** list,int data,int index)//插入元素,用*list将head指针和next统一表示
    {
        if(index<0 || index>lenlist(*list))return;//判断非法输入
        Node * newnode=(Node *)malloc(sizeof(Node));//创建
        newnode->data=data;
        newnode->next=NULL;
        while(index--)list=&((*list)->next);//插入
        newnode->next=*list;
        *list=newnode;
    }

    尾部增加元素

    void pushback(Node ** head,int data)//尾插,同上
    {
        Node * newnode=(Node *)malloc(sizeof(Node));//创建
        newnode->data=data;
        newnode->next=NULL;
        while(*head!=NULL)head=&((*head)->next);//插入
        *head=newnode;
    }

    清空链表

    void freelist(Node ** head)//清空链表
    {
        Node * temp=*head;
        Node * ttemp;
        *head=NULL;//指针设为空
        while(temp!=NULL)//释放
        {
            ttemp=temp;
            temp=temp->next;
            free(ttemp);
        }
    }

    删除

    void deletelist(Node ** list,int data)//删除链表节点
    {
        Node * temp;//作用只是方便free
        while((*list)->data!=data && (*list)->next!=NULL)list=&((*list)->next);
        if((*list)->data==data){
            temp=*list;
            *list=(*list)->next;
            free(temp);
        }
    }

    查找

    Node * findnode(Node ** list,int data)//查找,返回指向节点的指针,若无返回空
    {
        while((*list)->data!=data && (*list)!=NULL) list=&((*list)->next);
        return *list;
    }

    改值

    void change(Node ** list,int data,int temp)//改变
    {
        while((*list)->data!=data && (*list)->next!=NULL)list=&((*list)->next);
        if((*list)->data==data)(*list)->data=temp;
    }

     

    最后测试

    int main(void)//测试
    {
        Node * head=NULL;
        Node ** gg=&head;
        int i;
        for(i=0;i<10;i++)pushback(gg,i);
        printf("链表元素依次为: ");
        printlist(head);
        printf("长度为%d\n",lenlist(head));
        freelist(gg);
        printf("释放后长度为%d\n",lenlist(head));
        for(i=0;i<10;i++)pushback(gg,i);
        deletelist(gg,0);//头
        deletelist(gg,9);//尾
        deletelist(gg,5);
        deletelist(gg,100);//不存在
        printf("再次创建链表,删除节点后\n");
        printlist(head);
        freelist(gg);
        for(i=0;i<5;i++)pushback(gg,i);
        insertlist(gg,5,0);//头
        insertlist(gg,5,5);
        insertlist(gg,5,7);//尾
        insertlist(gg,5,10);//不存在
        printlist(head);
        printf("找到%d\n把3变为100",*findnode(gg,5));
        change(gg,3,100);
        change(gg,11111,1);//不存在
        printlist(head);
    }
    

    约瑟夫环-(数组、循环链表、数学) 

    约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3...n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依规律重复下去,直到圆桌周围的人全部出列。

     

    约瑟夫环运作如下:

    1、一群人围在一起坐成环状(如:N)

    2、从某个编号开始报数(如:S)

    3、数到某个数(如:M)的时候,此人出列,下一个人重新报数

    4、一直循环,直到所有人出列  ,约瑟夫环结束

    模拟过程,求出最后的人。

    把数组看成一个环,从第s个元素开始按m-1间隔删除元素,重复过程,直到元素全部去掉。

     

    void Josephus(int a[],int n,int m,int s)
    {
        int i,j;
        int k=n;
        for(i=0;i<n;i++)a[i]=i+1;//编号
        i=(s+n-1)%n;
        while(k)
        {
            for(j=1;j<m;j++)i=(i+1)%k;//依次报数,头尾相连
            printf("%d\n",a[i]);//出局
            for(j=i+1;j<k;j++)a[j-1]=a[j];//删除本节点
            k--;
        }
        //模拟结束,最后输出的就是留下的人
    }

     

    可以用带头单循环链表来求解:

    也是一样的,只是实现不同,给出核心代码:

        while(k)
        {
            for(j=1;j<m;j++)
            {
                pr=p;
                p=p->link;
                if(p==head)//头结点跳过
                {
                    pr=p;
                    p=p->link;
                }
            }
            k--;
            //打印
            pr->link=p->link;//删结点
            free(p);
            p=pr->link;//从下一个继续
        }

    双向循环链表也可以解,和单链表类似,只是不需要保持前趋指针。

     

    数学可解:

    效率最高

    
    int check_last_del(int n,int m)
    {
    	int i = 1;
    	int ret = 0;
    	for (i = 2; i<=n;i++)
            ret = (ret + m) %i;
    	return ret+1;//因为ret是从0到n-1,最后别忘了加1。
    }
    

    线性表表示集合

    集合我们高中都学过吧?

    最重要的几个特点:元素不能重复、各个元素之间没有关系、没有顺序

    集合内的元素可以是单元素或者是集合。

    对集合的操作:交集并集差集等,还有对自身的加减等。

    需要频繁的加减元素,所以顺序存储效率较低,但是我们还是说一下是怎么实现的:

        用01向量表示集合,因为现实中任何一个有穷集合都能对应到一个0、1、2.....n这么一个序列中。所以可以对应过来,每位的01代表这个元素存在与否即可。

    链接存储表示使用有序链表来实现,虽然集合是无序的,但是我们的链表可以是有序的。可以按升序排列。而链表理论上可以无限增加,所以链表可以表示无限集。

    下面我们来实现一下:

    我们定义一个节点:

    typedef int ElemType;
    typedef struct SetNode{//节点定义
        ElemType data;//数据
        struct SetNode * link;
    }*LinkedSet//集合定义

    然后要实现那些操作了,首先想插入吧:我们对于一个新元素,查找集合中是否存在,存在就不插入,不存在就插入到查找失败位置。

    删除也简单,查找存在就删除。

     

    我们说两个集合的操作:

    求两个集合的并:

    两个链表,都是升序。把他们去重合并即可。

    其实和链表归并的merge过程是一样的,只是相等的时候插入一个,两个指针都向后走就行了。

    我就再写一遍吧。

    void UnionSet(LinkedSet & A,LinkedSet & B,LinkedSet & C)
    {
        SetNode *pa=A->link,*pb=B->link,*pc=C;
        while(pa && pb)//都不为空
        {
            if(pa->data==pb->data)//相等,插一次,两边向后
            {
                pc->link=new SetNode;
                pc->data=pa->data;
                pa=pa->link;
                pb=pb->link;
            }
            else if(pa->data<pb->data)//插小的,小的向后
            {
                pc->link=new SetNode;
                pc->data=pa->data;
                pa=pa->link;
            }
            else
            {
                pc->link=new SetNode;
                pc->data=pb->data;
                pb=pb->link;
            }
            pc=pc->link;//注意指针
        }
        if(pa)p=pa;//剩下的接上
        else p=pb;//只执行一个
        while(p)//依次复制
        {
            pc->link=new SetNode;
            pc->data=p->data;
            pc=pc->link;
            p=p->link;
        }
        pc->link=NULL;
    }

    求两个集合的交,更简单,还是这三种情况,谁小谁向后,相等才插入。

    void UnionSet(LinkedSet & A,LinkedSet & B,LinkedSet & C)
    {
        SetNode *pa=A->link,*pb=B->link,*pc=C;
        while(pa && pb)//都不为空
        {
            if(pa->data==pb->data)//相等,插一次,两边向后
            {
                pc->link=new SetNode;
                pc->data=pa->data;
                pa=pa->link;
                pb=pb->link;
                pc=pc->link;//注意指针,就不是每次都向后了,只有插入才向后
            }
            else if(pa->data<pb->data)//小的向后
            {
                pa=pa->link;
            }
            else
            {
                pb=pb->link;
            }
        }
        pc->link=NULL;
    }

    求两个集合的差:高中可能没学这个概念,其实就是A-B,就是B中的元素,A都不能有了。

    运算你可以把B元素全过一遍,A中有就去掉,但是这样时间复杂度太高了,我们需要O(A+B)而不是O(A*B)

    因为有序,很好操作,还是两个指针,

    如果AB相同,都向后移。

    或者,B小,B就向后移。

    如果A小,说明B中不含这个元素,我们把它复制到结果链表里。

     

    思想还行,实在懒得写了,有时间再说吧。

     线性表实现一元多项式操作

     

    数组存放:

    不需要记录幂,下标就是。

    比如1,2,3,5表示1+2x+3x^2+5x^3

    有了思路,我们很容易定义结构

    typedef struct node{
        float * coef;//系数数组
        int maxSize;//最大容量
        int order;//最高阶数
    }Polynomial;

    先实现求和:我们想求两个式子a+b,结果存在c中。

    逻辑很简单,就是相加啊。

    void Add(Polynomial & A,Polynomial & B,Polynomial & C)
    {
        int i;
        int m=A.order;
        int n=B.order;
        for(i=0;i<=m && i<=n;i++)//共有部分加一起
            C.coef[i]=A.coef[i]+B.coef[i];
        while(i<=m)//只会执行一个,作用是把剩下的放入c
            C.coef[i]=A.coef[i];
        while(i<=n)
            C.coef[i]=B.coef[i];
        C.order=(m>n)?m:n;//等于较大项
    }

    实现乘法:

    我们思考一下,两个多项式怎么相乘?

    把a中每一项都和b中每一项乘一遍就好了。

    高中知识

     

    void Mul(Polynomial & A,Polynomial & B,Polynomial & C)
    {
        int i;
        int m=A.order;
        int n=B.order;
        if(m+n>C.maxSize)
        {
            printf("超限");
            return;
        }
        for(i=0;i<=m+n;i++)//注意范围,是最高项的幂加起来
            C.coef[i]=0.0;
        for(i=0;i<=m;i++)
        {
            for(j=0;j<=n;j++)
            {
                C.coef[i+j]+=A.coef[i]*B.coef[j];
            }
        }
        C.order=m+n;//注意范围,是最高项的幂加起来
    }

     

    利用数组存放虽然简单,但是当幂相差很大时,会造成空间上的严重浪费(包括时间也是),所以我们考虑采用链表存储。

     

    我们思考一下如何存储和做运算。

     

    我们肯定要再用一个变量记录幂了。每个节点记录系数和指数。

    考虑如何相加:

    对于c,其实刚开始是空的,我们首先要实现一个插入功能,然后,遍历a和b,进一步利用插入函数来不断尾插。

    因为a和b都是升幂排列,所以相加的时候,绝对不会发生结果幂小而后遇到的情况,所以放心的一直插入就好了。

    具体实现也比较好想:a和b幂相等就加起来,不等就小的单独插入,然后指针向后移。

    加法就放老师写的代码吧,很漂亮的代码:(没和老师商量,希望不会被打)

    老师原地插的,都一样都一样

    老师原文:http://www.edu2act.net/article/shu-ju-jie-gou-xian-xing-biao-de-jing-dian-ying-yong/

    void AddPolyn(polynomial &Pa, polynomial &Pb)
    	//多项式的加法:Pa = Pa + Pb,利用两个多项式的结点构成“和多项式”。 
    {
    	LinkList ha = Pa;		//ha和hb分别指向Pa和Pb的头指针
    	LinkList hb = Pb;
    	LinkList qa = Pa->next;
    	LinkList qb = Pb->next;	//ha和hb分别指向pa和pb的前驱
    	while (qa && qb)		//如果qa和qb均非空
    	{
    		float sum = 0.0;
    		term a = qa->data;
    		term b = qb->data;
    		switch (cmp(a,b))
    		{
    		case -1:	//多项式PA中当前结点的指数值小
    			ha = qa;
    			qa = qa->next;
    			break;
    		case 0:		//两者指数值相等
    			sum = a.coef + b.coef;
    			if(sum != 0.0)
    			{	//修改多项式PA中当前结点的系数值
    				qa->data.coef = sum;
    				ha = qa;
    			}else
    			{	//删除多项式PA中当前结点
    				DelFirst(ha, qa);
    				free(qa);
    			}
    			DelFirst(hb, qb);
    			free(qb);
    			qb = hb->next;
    			qa = ha->next;
    			break;
    		case 1:
    			DelFirst(hb, qb);
    			InsFirst(ha, qb);
    			qb = hb->next;
    			ha = ha->next;
    			break;
    		}//switch
    	}//while
    	if(!ListEmpty(Pb))
    		Append(Pa,qb);
    	DestroyList(hb);
    
    }//AddPolyn

    对于乘法,我们就不能一直往后插了,因为遍历两个式子,可能出现幂变小的情况。所以我们要实现一个插入函数,如果c中有这一项,就加起来,没这一项就插入。

    我们先实现插入函数:(哦,对了,我没有像老师那样把系数和指数再定义一个结构体,都放一起了。还有next我写的link,还有点别的不一样,都无伤大雅,绝对能看懂)

    void Insert(Polynomial &L,float c,int e)//系数c,指数e
    {
        Term * pre=L;
        Term * p=L->link;
        while(p && p->exp<e)//查找
        {
            pre=p;
            p=p->link;
        }
        if(p->exp==e)//如果有这一项
        {
            if(p->coef+c)//如果相加是0了,就删除节点
            {
                pre->link=p->link;
                free(p);
            }
            else//相加不是0,就合并
            {
                p->coef+=c;
            }
        }
        else//如果没这一项,插入就好了,链表插入写了很多遍了
        {
                Term * pc=new Term;//创建
                pc->exp=e;
                pc->coef=c;
                pre->link=pc;
                pc->link=p;        
        }
    }

    插入写完了,乘法就好实现了,还是两个循环,遍历a和b,只是最后调用Insert方法实现就ok

    insert(c,乘系数,加幂)

     

    拓展:一维数组可以模拟一元多项式。类似的,二维数组可以模拟二元多项式。实现以后有时间写了再放链接。

     

     

    链表环问题

    1.判断单链表是否有环

      使用两个slow, fast指针从头开始扫描链表。指针slow 每次走1步,指针fast每次走2步。如果存在环,则指针slow、fast会相遇;如果不存在环,指针fast遇到NULL退出。

      就是所谓的追击相遇问题:

        

    2.求有环单链表的环长

       在环上相遇后,记录第一次相遇点为Pos,之后指针slow继续每次走1步,fast每次走2步。在下次相遇的时候fast比slow正好又多走了一圈,也就是多走的距离等于环长。

      设从第一次相遇到第二次相遇,设slow走了len步,则fast走了2*len步,相遇时多走了一圈:

        环长=2*len-len。

    3.求有环单链表的环连接点位置

      第一次碰撞点Pos到连接点Join的距离=头指针到连接点Join的距离,因此,分别从第一次碰撞点Pos、头指针head开始走,相遇的那个点就是连接点。

         

      在环上相遇后,记录第一次相遇点为Pos,连接点为Join,假设头结点到连接点的长度为LenA,连接点到第一次相遇点的长度为x,环长为R

        第一次相遇时,slow走的长度 S = LenA + x;

        第一次相遇时,fast走的长度 2S = LenA + n*x;

        所以可以知道,LenA + x =  n*R;  LenA = n*R -x;

    4.求有环单链表的链表长

       上述2中求出了环的长度;3中求出了连接点的位置,就可以求出头结点到连接点的长度。两者相加就是链表的长度。

     

    编程实现:

      下面是代码中的例子:

      

      具体代码如下:

    #include <stdio.h>
    #include <stdlib.h>
    typedef struct node{
        int value;
        struct node *next;
    }LinkNode,*Linklist;
    
    /// 创建链表(链表长度,环节点起始位置)
    Linklist createList(){
        Linklist head = NULL;
        LinkNode *preNode = head;
        LinkNode *FifthNode = NULL;
        for(int i=0;i<6;i++){
            LinkNode *tt = (LinkNode*)malloc(sizeof(LinkNode));
            tt->value = i;
            tt->next = NULL;
            if(preNode == NULL){
                head = tt;
                preNode = head;
            }
            else{
                preNode->next =tt;
                preNode = tt;
            }
    
            if(i == 3)
                FifthNode = tt;
        }
        preNode->next = FifthNode;
        return head;
    }
    
    ///判断链表是否有环
    LinkNode* judgeRing(Linklist list){
        LinkNode *fast = list;
        LinkNode *slow = list;
    
        if(list == NULL)
            return NULL;
    
        while(true){
            if(slow->next != NULL && fast->next != NULL && fast->next->next != NULL){
                slow = slow->next;
                fast = fast->next->next;
            }
            else
                return NULL;
    
            if(fast == slow)
                return fast;
        }
    }
    
    ///获取链表环长
    int getRingLength(LinkNode *ringMeetNode){
        int RingLength=0;
        LinkNode *fast = ringMeetNode;
        LinkNode *slow = ringMeetNode;
        for(;;){
            fast = fast->next->next;
            slow = slow->next;
            RingLength++;
            if(fast == slow)
                break;
        }
        return RingLength;
    }
    
    ///获取链表头到环连接点的长度
    int getLenA(Linklist list,LinkNode *ringMeetNode){
        int lenA=0;
        LinkNode *fast = list;
        LinkNode *slow = ringMeetNode;
        for(;;){
            fast = fast->next;
            slow = slow->next;
            lenA++;
            if(fast == slow)
                break;
        }
        return lenA;
    }
    
    ///环起始点
    ///如果有环, 释放空空间时需要注意.
    LinkNode* RingStart(Linklist list, int lenA){
        if (!list || lenA <= 0){
            return NULL;
        }
    
        int i = 0;
        LinkNode* tmp = list;
        for ( ; i < lenA; ++i){
            if (tmp != NULL){
                tmp = tmp->next;
            }
        }
    
        return (i == lenA)? tmp : NULL;
    }
    
    ///释放空间
    int freeMalloc(Linklist list, LinkNode* ringstart){
        bool is_ringstart_free = false; //环起始点只能被释放空间一次
        LinkNode *nextnode = NULL;
    
        while(list != NULL){
            nextnode = list->next;
            if (list == ringstart){ //如果是环起始点
                if (is_ringstart_free)
                    break;  //如果第二次遇到环起始点addr, 表示已经释放完成
                else
                    is_ringstart_free = true;   //记录已经释放一次
            }
            free(list);
            list = nextnode;
        }
    
        return 0;
    }
    
    int main(){
        Linklist list = NULL;
        LinkNode *ringMeetNode  = NULL;
        LinkNode *ringStartNode = NULL;
    
        int LenA       = 0;
        int RingLength = 0;
    
        list = createList();
        ringMeetNode = judgeRing(list); //快慢指针相遇点
    
        if(ringMeetNode == NULL)
            printf("No Ring\n");
        else{
            printf("Have Ring\n");
            RingLength = getRingLength(ringMeetNode);   //环长
            LenA = getLenA(list,ringMeetNode);
    
            printf("RingLength:%d\n", RingLength);
            printf("LenA:%d\n", LenA);
            printf("listLength=%d\n", RingLength+LenA);
        }
    
        ringStartNode = RingStart(list, LenA);  //获取环起始点
        freeMalloc(list, ringStartNode);    //释放环节点, 有环时需要注意. 采纳5楼建议
        return 0;
    }

     

    移除链表元素

     

    删除链表中等于给定值 val 的所有节点。

    示例:

    输入: 1->2->6->3->4->5->6, val = 6
    输出: 1->2->3->4->5

    思路:就删呗,注意第一个数可能会被删

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
    	public ListNode removeElements(ListNode head, int val) {
    		ListNode p = new ListNode(-1);
    		p.next = head;
    		//因为要删除的可能是链表的第一个元素,所以用一个h节点来做处理
    		ListNode h = p;
    		while(p.next!=null) {
    			if(p.next.val==val) {
    				p.next = p.next.next;
    			}else{
                    p = p.next;
                }	
    		}
    		return h.next;
    	}
    }

    回文链表

    请判断一个链表是否为回文链表。

    示例 1:

    输入: 1->2
    输出: false
    示例 2:

    输入: 1->2->2->1
    输出: true
    进阶:
    你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

    思路:逆置前一半,然后从中心出发开始比较即可。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public boolean isPalindrome(ListNode head) {
            if(head == null || head.next == null) {
                return true;
            }
            ListNode slow = head, fast = head;
            ListNode pre = head, prepre = null;
            while(fast != null && fast.next != null) {
                pre = slow;
                slow = slow.next;
                fast = fast.next.next;
                pre.next = prepre;
                prepre = pre;
            }
            if(fast != null) {
                slow = slow.next;
            }
            while(pre != null && slow != null) {
                if(pre.val != slow.val) {
                    return false;
                }
                pre = pre.next;
                slow = slow.next;
            }
            return true;
        }
    }

    链表表示整数,相加

    思路:就模仿加法即可。。。题目还贴心的给把顺序反过来了。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
            ListNode ans=new ListNode(0);
            ListNode tempA=l1;
            ListNode tempB=l2;
            ListNode temp=ans;
            int out=0;
            while(tempA!=null || tempB!=null){
                int a=tempA!=null?tempA.val:0;
                int b=tempB!=null?tempB.val:0;
                ans.next=new ListNode((a+b+out)%10);
                ans=ans.next;
                out=(a+b+out)/10;
                if(tempA!=null)tempA=tempA.next;
                if(tempB!=null)tempB=tempB.next;
            }
            if(out!=0){
              ans.next=new ListNode(out);  
            }
            return temp.next;
        }
    }

    LRU

    LRU全称是Least Recently Used,即最近最久未使用的意思。

    LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。(这一段是找的,让大家理解一下什么是LRU)。

     

    说一下我们什么时候见到过LRU:其实老师们肯定都给大家举过这么个例子:你在图书馆,你把书架子里的书拿到桌子上。。但是桌子是有限的,你有时候不得不把一些书放回去。这就相当于内存和硬盘。这个例子都说过吧?

    LRU就是记录你最长时间没看过的书,就把它放回去。在cache那里见过吧

     

    然后最近在研究redis,又看到了这个LRU,所以就想写一下吧。

    题目:设计一个结构,这个结构可以查询K-V,但是容量有限,当存不下的时候就要把用的年代最久远的那个东西扔掉。

    其实思路很简单,我们维护一个双向链表即可,get也就是使用了,我们就把把它提到最安全的位置。新来的KV就依次放即可。

    我们就先写这个双向链表结构

    先写节点结构:

    	public static class Node<V> {
    		public V value;
    		public Node<V> last;//前
    		public Node<V> next;//后
    
    		public Node(V value) {
    			this.value = value;
    		}
    	}

    然后写双向链表结构: 我们没必要把链表操作都写了,分析一下,我们只有三个操作:

    1、加节点

    2、使用了某个节点就把它调到尾,代表优先级最高

    3、把优先级最低的移除,也就是去头部

    (不会的,翻我之前的链表操作都有写)

    	public static class NodeDoubleLinkedList<V> {
    		private Node<V> head;//头
    		private Node<V> tail;//尾
    
    		public NodeDoubleLinkedList() {
    			this.head = null;
    			this.tail = null;
    		}
    
    		public void addNode(Node<V> newNode) {
    			if (newNode == null) {
    				return;
    			}
    			if (this.head == null) {//头空
    				this.head = newNode;
    				this.tail = newNode;
    			} else {//头不空
    				this.tail.next = newNode;
    				newNode.last = this.tail;//注意让本节点前指针指向旧尾
    				this.tail = newNode;//指向新尾
    			}
    		}
    /*某个点移到最后*/
    		public void moveNodeToTail(Node<V> node) {
    			if (this.tail == node) {//是尾
    				return;
    			}
    			if (this.head == node) {//是头
    				this.head = node.next;
    				this.head.last = null;
    			} else {//中间
    				node.last.next = node.next;
    				node.next.last = node.last;
    			}
    			node.last = this.tail;
    			node.next = null;
    			this.tail.next = node;
    			this.tail = node;
    		}
    /*删除第一个*/
    		public Node<V> removeHead() {
    			if (this.head == null) {
    				return null;
    			}
    			Node<V> res = this.head;
    			if (this.head == this.tail) {//就一个
    				this.head = null;
    				this.tail = null;
    			} else {
    				this.head = res.next;
    				res.next = null;
    				this.head.last = null;
    			}
    			return res;
    		}
    
    	}

    链表操作封装完了就要实现这个结构了。

    具体思路代码注释

    	public static class MyCache<K, V> {
    		//为了kv or vk都能查
    		private HashMap<K, Node<V>> keyNodeMap;
    		private HashMap<Node<V>, K> nodeKeyMap;
    		//用来做优先级
    		private NodeDoubleLinkedList<V> nodeList;
    		private int capacity;//容量
    
    		public MyCache(int capacity) {
    			if (capacity < 1) {//你容量连1都不给,捣乱呢
    				throw new RuntimeException("should be more than 0.");
    			}
    			this.keyNodeMap = new HashMap<K, Node<V>>();
    			this.nodeKeyMap = new HashMap<Node<V>, K>();
    			this.nodeList = new NodeDoubleLinkedList<V>();
    			this.capacity = capacity;
    		}
    
    		public V get(K key) {
    			if (this.keyNodeMap.containsKey(key)) {
    				Node<V> res = this.keyNodeMap.get(key);
    				this.nodeList.moveNodeToTail(res);//使用过了就放到尾部
    				return res.value;
    			}
    			return null;
    		}
    
    		public void set(K key, V value) {
    			if (this.keyNodeMap.containsKey(key)) {
    				Node<V> node = this.keyNodeMap.get(key);
    				node.value = value;//放新v
    				this.nodeList.moveNodeToTail(node);//我们认为放入旧key也是使用过
    			} else {
    				Node<V> newNode = new Node<V>(value);
    				this.keyNodeMap.put(key, newNode);
    				this.nodeKeyMap.put(newNode, key);
    				this.nodeList.addNode(newNode);//加进去
    				if (this.keyNodeMap.size() == this.capacity + 1) {
    					this.removeMostUnusedCache();//放不下就去掉优先级最低的
    				}
    			}
    		}
    
    		private void removeMostUnusedCache() {
    			//删除头
    			Node<V> removeNode = this.nodeList.removeHead();
    			K removeKey = this.nodeKeyMap.get(removeNode);
    			//删除掉两个map中的记录
    			this.nodeKeyMap.remove(removeNode);
    			this.keyNodeMap.remove(removeKey);
    		}
    	}

    LFU

    请你为 最不经常使用(LFU)缓存算法设计并实现数据结构。可以自行百度介绍,非常著名的结构

    实现 LFUCache 类:

    LFUCache(int capacity) - 用数据结构的容量 capacity 初始化对象
    int get(int key) - 如果键存在于缓存中,则获取键的值,否则返回 -1。
    void put(int key, int value) - 如果键已存在,则变更其值;如果键不存在,请插入键值对。当缓存达到其容量时,则应该在插入新项之前,使最不经常使用的项无效。在此问题中,当存在平局(即两个或更多个键具有相同使用频率)时,应该去除 最久未使用 的键。
    注意「项的使用次数」就是自插入该项以来对其调用 get 和 put 函数的次数之和。使用次数会在对应项被移除后置为 0 。

    为了确定最不常使用的键,可以为缓存中的每个键维护一个 使用计数器 。使用计数最小的键是最久未使用的键。

    当一个键首次插入到缓存中时,它的使用计数器被设置为 1 (由于 put 操作)。对缓存中的键执行 get 或 put 操作,使用计数器的值将会递增。


    你可以为这两种操作设计时间复杂度为 O(1) 的实现吗?

    // 缓存的节点信息
    struct Node {
        int key, val, freq;
        Node(int _key,int _val,int _freq): key(_key), val(_val), freq(_freq){}
    };
    class LFUCache {
        int minfreq, capacity;
        unordered_map<int, list<Node>::iterator> key_table;
        unordered_map<int, list<Node>> freq_table;
    public:
        LFUCache(int _capacity) {
            minfreq = 0;
            capacity = _capacity;
            key_table.clear();
            freq_table.clear();
        }
        
        int get(int key) {
            if (capacity == 0) return -1;
            auto it = key_table.find(key);
            if (it == key_table.end()) return -1;
            list<Node>::iterator node = it -> second;
            int val = node -> val, freq = node -> freq;
            freq_table[freq].erase(node);
            // 如果当前链表为空,我们需要在哈希表中删除,且更新minFreq
            if (freq_table[freq].size() == 0) {
                freq_table.erase(freq);
                if (minfreq == freq) minfreq += 1;
            }
            // 插入到 freq + 1 中
            freq_table[freq + 1].push_front(Node(key, val, freq + 1));
            key_table[key] = freq_table[freq + 1].begin();
            return val;
        }
        
        void put(int key, int value) {
            if (capacity == 0) return;
            auto it = key_table.find(key);
            if (it == key_table.end()) {
                // 缓存已满,需要进行删除操作
                if (key_table.size() == capacity) {
                    // 通过 minFreq 拿到 freq_table[minFreq] 链表的末尾节点
                    auto it2 = freq_table[minfreq].back();
                    key_table.erase(it2.key);
                    freq_table[minfreq].pop_back();
                    if (freq_table[minfreq].size() == 0) {
                        freq_table.erase(minfreq);
                    }
                } 
                freq_table[1].push_front(Node(key, value, 1));
                key_table[key] = freq_table[1].begin();
                minfreq = 1;
            } else {
                // 与 get 操作基本一致,除了需要更新缓存的值
                list<Node>::iterator node = it -> second;
                int freq = node -> freq;
                freq_table[freq].erase(node);
                if (freq_table[freq].size() == 0) {
                    freq_table.erase(freq);
                    if (minfreq == freq) minfreq += 1;
                }
                freq_table[freq + 1].push_front(Node(key, value, freq + 1));
                key_table[key] = freq_table[freq + 1].begin();
            }
        }
    };

    合并链表

     

    将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

    示例:

    输入:1->2->4, 1->3->4
    输出:1->1->2->3->4->4

     

    思路:链表归并。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
            ListNode head=new ListNode(0);
            ListNode temp=head;
            while(l1!=null && l2!=null){
                if(l1.val>l2.val){
                    temp.next=l2;
                    l2=l2.next;
                }else{
                    temp.next=l1;
                    l1=l1.next;  
                }
                temp=temp.next;
            }
            if(l1!=null){
                temp.next=l1;
            }else{
                temp.next=l2;
            }
            return head.next;
        }
    }

    反转链表

    反转一个单链表。

    示例:

    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL
     

    经典题不解释,画图自己模拟记得清楚

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode reverseList(ListNode head) {
            ListNode prev = null;
            ListNode curr = head;
            while (curr != null) {
                ListNode nextTemp = curr.next;
                curr.next = prev;
                prev = curr;
                curr = nextTemp;
            }
            return prev;
        }
    }

     反转链表2

    反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

    说明:
    1 ≤ m ≤ n ≤ 链表长度。

    示例:

    输入: 1->2->3->4->5->NULL, m = 2, n = 4
    输出: 1->4->3->2->5->NULL

    思路:反转链表,只不过是反转一部分,注意这一部分逆序之前做好记录,方便逆序完后可以链接上链表的其他部分。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode reverseBetween(ListNode head, int m, int n) {
            if (head == null) return null;
            ListNode cur = head, prev = null;
            while (m > 1) {
                prev = cur;
                cur = cur.next;
                m--;
                n--;
            }
            ListNode con = prev, tail = cur;
            ListNode third = null;
            while (n > 0) {
                third = cur.next;
                cur.next = prev;
                prev = cur;
                cur = third;
                n--;
            }
            if (con != null) {
                con.next = prev;
            } else {
                head = prev;
            }
            tail.next = cur;
            return head;
        }
    }

    对链表排序

    丢人,我就是按插入排序老老实实写的啊。。。。

    别人肯定map了hhh。

    对链表进行插入排序。


    插入排序的动画演示如上。从第一个元素开始,该链表可以被认为已经部分排序(用黑色表示)。
    每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将其插入到已排好序的链表中。

     

    插入排序算法:

    插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
    每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
    重复直到所有输入数据插入完为止。
     

    示例 1:

    输入: 4->2->1->3
    输出: 1->2->3->4
    示例 2:

    输入: -1->5->3->4->0
    输出: -1->0->3->4->5

    思路:按插入排序思路写就可以啦,只是注意链表操作,比数组麻烦很多。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode insertionSortList(ListNode head) {
            ListNode ans=new ListNode(-1);
            ListNode temp=null;//要插入的地方
            ListNode key=null;//要插入的值
            while(head!=null){
                key=head;
                temp=ans;
                while(temp.next!=null && key.val>temp.next.val){
                    temp=temp.next;
                }
                head=head.next;
                key.next=temp.next;
                temp.next=key;
            }
            return ans.next;
    
        }
    }

    旋转链表

    给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

    示例 1:

    输入: 1->2->3->4->5->NULL, k = 2
    输出: 4->5->1->2->3->NULL
    解释:
    向右旋转 1 步: 5->1->2->3->4->NULL
    向右旋转 2 步: 4->5->1->2->3->NULL
    示例 2:

    输入: 0->1->2->NULL, k = 4
    输出: 2->0->1->NULL
    解释:
    向右旋转 1 步: 2->0->1->NULL
    向右旋转 2 步: 1->2->0->NULL
    向右旋转 3 步: 0->1->2->NULL
    向右旋转 4 步: 2->0->1->NULL

    思路:找准断点,直接调指针即可。

    注意:长度可能超过链表长度,要取模。

    /**
     * Definition for singly-linked list.
     * public class ListNode {
     *     int val;
     *     ListNode next;
     *     ListNode(int x) { val = x; }
     * }
     */
    class Solution {
        public ListNode rotateRight(ListNode head, int k) {
            if(head==null){
                return null;
            }
            int len=0;
            ListNode temp=head;
            while(temp!=null){
                temp=temp.next;
                len++;
            }
            k=k%len;
            ListNode node=head;
            ListNode fast=head;
            while(k-->0){
                fast=fast.next;
            }
            while(fast.next!=null){
                node=node.next;
                fast=fast.next;
            }
            fast.next=head;
            ListNode ans=node.next;
            node.next=null;
            return ans;
    
        }
    }

     数组实现栈

    学习了改进,利用define typedef比上次写的链表更容易改变功能,方便维护,代码更健壮。

    大佬别嫌弃,萌新总是很笨,用typedef都想不到。

    #include<stdio.h>
    #include<stdbool.h>
    #define maxsize 10
    typedef int datatype;
    typedef struct stack
    {
        datatype data[maxsize];
        int top;
    }Stack;
    Stack s;
    void init()//初始化
    {
        s.top=-1;
    }
    int Empty()//是否空
    {
        if(s.top==-1)return 1;
        return 0;
    }
    int full()//是否满
    {
        if(s.top==maxsize-1)return 1;
        return 0;
    }
    void Push(datatype element)//入栈
    {
        if(!full()){
            s.top++;
            s.data[s.top]=element;
        }
        else printf("栈满\n");
    }
    void Pop()//出栈
    {
        if(!Empty()) s.top--;
        else printf("栈空\n");
    }
    datatype Top()//取栈顶元素
    {
        if(!Empty()) return s.data[s.top];
        printf("栈空\n");
    }
    void Destroy()//销毁
    {
        s.top=-1;
    }

    测试不写了。

     

    链表实现栈

     

    栈,是操作受限的线性表,只能在一端进行插入删除。

    其实就是带尾指针的链表,尾插

    #include <stdio.h>
    #include <stdlib.h>
    #define OK 1
    #define ERROR 0
    #define Status int
    #define SElemType int
    //只在头部进行插入和删除(不带头结点)
    typedef struct LNode
    {
    	SElemType data;
    	struct LNode *next;
    }LNode, *LinkList;
    
    typedef struct 
    {
    	LNode *top;
    	LNode *base;
    	int length;
    }LinkStack;
    
    
    Status InitStack(LinkStack &S)
    {
    	S.base = NULL;
    	S.top = NULL;
    	S.length = 0;
    	return OK;
    }
    
    Status GetTop(LinkStack S, SElemType &e)
    {
    	if(S.length == 0)
    		return ERROR;
    	e = S.top->data ;
    	return OK;
    }
    
    Status Push(LinkStack &S, SElemType e)
    {
    	LNode *newNode = (LNode *)malloc(sizeof(LNode));
    	newNode->data = e;
    	newNode->next = S.top;
    	S.top = newNode;
    	if(!S.base)
    		S.base = newNode;
    	++S.length;
    	return OK;
    }
    
    Status Pop(LinkStack &S, SElemType &e)
    {
    	LNode *p = S.top;
    	if(S.length == 0)
    		return ERROR;
    	e = S.top->data;
    	S.top = S.top->next;
    	free(p);
    	--S.length;
    	return OK;
    }
    
    void PrintStack(LinkStack S)
    {
    	LNode *p = S.top;
    	printf("由栈顶到栈底:");
    	while (p)
    	{
    		printf("%d  ",p->data);
    		p = p->next;
    	}
    	printf("\n");
    }
    
    
    int main(void)
    {
    	LinkStack LS;
    	InitStack(LS);
    	Push(LS,11);
    	Push(LS,22);
    	Push(LS,33);
    	Push(LS,44);
    	Push(LS,55);
    	PrintStack(LS);
    	SElemType e ;
    	GetTop(LS , e);
    	printf("栈顶元素是: %d\n",e);
    	Pop(LS,e);
    	PrintStack(LS);
    	Pop(LS,e);
    	PrintStack(LS);
    
    
    
    	return 0;
    }

    数组实现队列

     

    数组实现队列结构:

    相对栈结构要难搞一些,队列的先进先出的,需要一个数组和三个变量,size记录已经进来了多少个元素,不需要其它萌新看不懂的知识。

    触底反弹,头尾追逐的感觉。

    循环使用数组。

    具体解释一下触底反弹:当我们的队头已经到了数组的底,我们就把对头设为数组的第一个元素,对于队尾也是一样。实现了对数组的循环使用。

    #include<stdio.h>
    #include<stdbool.h>
    #define maxsize 10
    typedef int datatype;
    typedef struct queue
    {
        datatype arr[maxsize];
        int a,b,size;//头、尾、数量
    }queue;
    queue s;
    void init()//初始化
    {
        s.a=0;
        s.b=0;
        s.size=0;
    }
    int Empty()//判空
    {
        if(s.size==0)return 1;
        return 0;
    }
    int full()//判满
    {
        if(s.size==maxsize)return 1;
        return 0;
    }
    datatype peek()//查看队头
    {
        if(s.size!=0)return s.arr[s.a];
        printf("queue is null\n");
    }
    datatype poll()//弹出队头
    {
        int temp=s.a;
        if(s.size!=0)
        {
            s.size--;
            s.a=s.a==maxsize-1? 0 :s.a+1;//触底反弹
            return s.arr[temp];
        }
        printf("queue is null\n");
    }
    int push(datatype obj)//放入队尾
    {
        if(s.size!=maxsize)
        {
            s.size++;
            s.arr[s.b]=obj;
            s.b=s.b==maxsize-1? 0 : s.b+1;//触底反弹
            return 1;
        }
        printf("queue is full\n");
        return 0;
    }
    //测试
    int main()
    {
        int i;
        init();
        if(Empty())printf("null\n");
        for(i=0;i<20;i++)push(i);
        while(!Empty())
        {
            printf("%d\n",poll());
        }
        printf("%d",poll());
    }
    

    链表实现队列

     

    这次写的还算正规,稍微压缩了一下代码,但是不影响阅读

    画个图帮助理解:

    F->0->0->0<-R

    第一个0不存数据 

     

    #include<stdio.h>
    #include<malloc.h>
    #include<stdlib.h>
    typedef int Elementype;//数据类型
    //节点结构
    typedef struct Node{
        Elementype Element;//数据域
        struct Node * Next;
    }NODE,*PNODE;
    
    //    定义队列结构体
    typedef struct QNode {
        PNODE Front;//队头
        PNODE Rear;//队尾
    } Queue, *PQueue;
    
    void init(PQueue queue)//初始化
    {//头尾指向同一内存空间//头结点,不存数据
        queue->Front = queue->Rear = (PNODE)malloc(sizeof(NODE));
        queue->Front->Next = NULL;//头结点指针为空
    }
    
    int isEmpty(PQueue queue)//判空·
    {
        if(queue->Front == queue->Rear)return 1;
        return 0;
    }
    
    void insert(PQueue queue,Elementype data)//入队
    {
        PNODE P = (PNODE)malloc(sizeof(NODE));//初始化
        P->Element = data;
        P->Next = NULL;
        queue->Rear->Next = P;//入队
        queue->Rear = P;
    }
    
    void delete(PQueue queue,int * val)//出队,用val返回值
    {
        if(isEmpty(queue))printf("队空");
        else
        {
            PNODE  P = queue->Front->Next;//前一元素
            *val = P->Element;//记录值
            queue->Front->Next = P->Next;//出队
            //注意一定要加上判断,手动模拟一下就明白了
            if(P==queue->Rear)queue->Rear = queue->Front;
            free(P);//注意释放
            P = NULL;
        }
    }
    
    void destroy(PQueue queue)//释放
    {
        //从头开始删
        while(queue->Front != NULL)//起临时指针作用,无需再用别的空间
        {
            queue->Rear = queue->Front->Next;
            free(queue->Front);
            queue->Front = queue->Rear;
        }
    }
    //测试
    int main(void)
    {
        int i;
        int e;
        Queue a;
        PQueue queue=&a;
        init(queue);
        for(i=0;i<10;i++)
            insert(queue,i);
        while(!isEmpty(queue))//遍历
        {
            delete(queue,&e);
            printf("%d ",e);
        }
        if(isEmpty(queue))printf("1\n");
        delete(queue,&e);
        destroy(queue);
    }

    双栈的实现

    利用栈底位置相对不变的特性,可以让两个顺序栈共享一个空间。

    具体实现方法大概有两种:

    一种是奇偶栈,就是所有下标为奇数的是一个栈,偶数是另一个栈。但是这样一个栈的最大存储就确定了,并没有起到互补空缺的作用,我们实现了也就没有太大意义。

    还有一种就是,栈底分别设在数组的头和尾。进栈往中间进就可以了。这样,整个数组存满了才会真的栈满。

     

    那我们直接开始代码实现

     

    首先定义结构体:

    typedef struct
    {
      int top[2], bot[2];    //栈顶和栈底指针
      int *V;      //栈数组
      int m;     //栈最大可容纳元素个数
    }DblStack;

     

    初始化双栈s,长度为n:

    void Init(DblStack &S,int n)
    {
        S.m = n;
        S.V = new int [n+10];
        S.bot[0] = S.top[0] = -1;
        S.bot[1] = S.top[1] = S.m;  
    }

    判空:

    int EmptyStack0(DblStack S)
    {
        if(S.top[0]==-1)return 0;
        else return 1;
    }
    int EmptyStack1(DblStack S)
    {
        if(S.top[1]==S.m)return 0;
        else return 1;
    }

    判满:(没有单独判断一个栈的,是判断整个储存空间还有没有地方)

    int FullStack(DblStack S)
    {
        if(S.top[1]-S.top[0]==1)return 1;
        else return 0;
    }

    进栈:

    void Push0(DblStack &S,int e)
    {
        if(S.top[1]-S.top[0]!=1)
        {
            S.top[0]++;
            S.V[S.top[0]] = e;
        }
    }
    
    void Push1(DblStack &S,int e)
    {
        if(S.top[1]-S.top[0] != 1)
        {
            S.top[1]--;
            S.V[S.top[1]] = e;
        }
    }

    出栈:

    void Pop0(DblStack &S,int &e)
    {
        if(S.top[0]!=-1)
        {
            e = S.V[S.top[0]];
            S.top[0]--;
        }
    }
    
    void Pop1(DblStack &S,int &e)
    {
        if(S.top[1]!=S.m)
        {
            e = S.V[S.top[1]];
            S.top[1]++;
        }
    }

     栈/队列 互相模拟实现

     

    用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

    思路:大概这么想:用一个辅助栈把进第一个栈的元素倒一下就好了。

    比如进栈1,2,3,4,5

    第一个栈:

    5

    4

    3

    2

    1

    然后倒到第二个栈里

    1

    2

    3

    4

    5

    再倒出来,顺序为1,2,3,4,5

    实现队列

    然后要注意的事情:

    1)栈2非空不能往里面倒数,顺序就错了。栈2没数再从栈1倒。

    2)栈1要倒就一次倒完,不倒完的话,进新数也会循序不对。

    import java.util.Stack;
     
    public class Solution {
        Stack<Integer> stack1 = new Stack<Integer>();
        Stack<Integer> stack2 = new Stack<Integer>();
         
        public void push(int node) {
            stack1.push(node);
        }
         
        public int pop() {
            if(stack1.empty()&&stack2.empty()){
                throw new RuntimeException("Queue is empty!");
            }
            if(stack2.empty()){
                while(!stack1.empty()){
                    stack2.push(stack1.pop());
                }
            }
            return stack2.pop();
        }
    }

     

    用两个队列实现栈,要求同上:

    这其实意义不是很大,有些数据结构书上甚至说两个队列不能实现栈。

    其实是可以的,只是时间复杂度较高,一个弹出操作时间为O(N)。

    思路:两个队列,编号为1和2.

    进栈操作:进1号队列

    出栈操作:把1号队列全弄到2号队列里,剩最后一个别压入,而是返回。

    最后还得把1和2号换一下,因为现在是2号有数,1号空。

     

    仅仅有思考价值,不实用。

    比如压入1,2,3

    队列1:1,2,3

    队列2:空

    依次弹出1,2,3:

    队列1里的23进入2号,3弹出

    队列1:空

    队列2:2,3

     

    队列2中3压入1号,2弹出

    队列1:3

    队列2:空

     

    队列1中只有一个元素,弹出。

     

    上代码:

    public class TwoQueueImplStack {
    	Queue<Integer> queue1 = new ArrayDeque<Integer>();
    	Queue<Integer> queue2 = new ArrayDeque<Integer>();
    //压入
    	public void push(Integer element){
    		//都为空,优先1
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    		//1为空,2有数据,放入2
    		if(queue1.isEmpty()){
    			queue2.add(element);
    			return;
    		}
    		//2为空,1有数据,放入1
    		if(queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    	}
    //弹出
    	public Integer pop(){
    		//两个都空,异常
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			try{
    				throw new Exception("satck is empty!");
    			}catch(Exception e){
    				e.printStackTrace();
    			}
    		}	
    		//1空,2有数据,将2中的数据依次放入1,最后一个元素弹出
    		if(queue1.isEmpty()){
    			while(queue2.size() > 1){
    				queue1.add(queue2.poll());
    			}
    			return queue2.poll();
    		}
    		
    		//2空,1有数据,将1中的数据依次放入2,最后一个元素弹出
    		if(queue2.isEmpty()){
    			while(queue1.size() > 1){
    				queue2.add(queue1.poll());
    			}
    			return queue1.poll();
    		}
    		
    		return (Integer)null;
    	}
    //测试
    	public static void main(String[] args) {
    		TwoQueueImplStack qs = new TwoQueueImplStack();
    		qs.push(2);
    		qs.push(4);
    		qs.push(7);
    		qs.push(5);
    		System.out.println(qs.pop());
    		System.out.println(qs.pop());
    		
    		qs.push(1);
    		System.out.println(qs.pop());
    	}
    }
    

     

    栈的排序

      一个栈中元素的类型为整型,现在想将该栈从顶到底按从大到小的顺序排序,只许申请一个栈。除此之外,可以申请新的变量,但是不能申请额外的数据结构,如何完成排序?

    思路:

        将要排序的栈记为stack,申请的辅助栈记为help.在stack上执行pop操作,弹出的元素记为cru.

          如果cru小于或等于help的栈顶元素,则将cru直接压入help.

          如果cru大于help的栈顶元素,则将help的元素逐一弹出,逐一压入stack,直到cru小于或等于help的栈顶元素,再将cru压入help.

    一直执行以上操作,直到stack中的全部元素压入到help,最后将heip中的所有元素逐一压入stack,完成排序。

     

    其实和维持单调栈的思路挺像的,只是弹出后没有丢弃,接着放。

    和基础排序也挺像。

     

    import java.util.Stack;
    public class a{
       public static void sortStackByStack(Stack<Integer> stack){
           Stack<Integer> help=new Stack<Integer>();
           while(!stack.isEmpty()){
               int cru=stack.pop();
               while(!help.isEmpty()&&help.peek()<cru){
                   stack.push(help.pop());
               }
               help.push(cru);
           }
           while (!help.isEmpty()) {
                 stack.push(help.pop());        
        }
       }
    }

    栈——括号匹配

    栈的应用,括号匹配。

    经典做法是,遇左括号压入,遇右括号判断,和栈顶配对就继续,不配对或者栈空就错了。最后判断是否为空。

    代码有些麻烦。

     

    我是遇左括号压对应的右括号,最后判断代码就会很简单:相等即可。

    class Solution {
    public:
        bool isValid(string s) {
            int len=s.size();
            stack<char> st;
            for(int i=0;i<len;i++){
                if(s[i]=='(')st.push(')');
                else if(s[i]=='[')st.push(']');
                else if(s[i]=='{')st.push('}');
                else if(st.empty())return false;
                else if(st.top()!=s[i])return false;
                else st.pop();
            }
            return st.empty();
        }
    };

    栈——表达式求值 

    今天把表达式求值给搞定吧。

     

    问题:给你个表达式,有加减乘除和小括号,让算出结果。

    我们假定计算式是正确的,并且不会出现除数为0等错误。

    py大法好啊,在保证可读性的前提下能压到一共就三十多行代码。

    其实能压到不到三十行,但是代码就不好看了。。。。

    计算函数:

    def getvalue(a, b, op):
        if op == "+":return a+b
        elif op == "-":return a-b
        elif op == "*":return a*b
        else:return a/b

     

    出栈一个运算符,两个数值,计算,将结果入data用于之后计算

    def process(data, opt):
        operator = opt.pop()
        num2 = data.pop()
        num1 = data.pop()
        data.append(getvalue(num1, num2, operator))

    比较符号优先级:
    乘除运算优先级比加减高。

    op1优先级比op2高返回True,否则返回False

    def compare(op1, op2):
        return op1 in ["*","/"] and op2 in ["+","-"]

    主函数:

    基本思路:

    处理每个数字为一个整数,处理每一项为一个单独的数字,把括号内处理为一个单独的数字。

    把式子处理为只有整数、加减的式子再最后计算。

    def calculate(s):
        data = []#数据栈
        opt = []#操作符栈
        i = 0  #表达式遍历的索引
        while i < len(s):
            if s[i].isdigit():  # 数字,入栈data
                start = i
                while i+1  < len(s) and s[i + 1].isdigit():
                    i += 1
                data.append(int(s[start: i + 1]))  # i为最后一个数字字符的位置
            elif s[i] == ")":  # 右括号,opt出栈,data出栈并计算,结果入data,直到左括号
                while opt[-1] != "(":
                    process(data,opt)#优先级高的一定先弹出
                opt.pop()  # 出栈的一定是左括号
            elif not opt or opt[-1] == "(":opt.append(s[i])#栈空,或栈顶为左括号,入opt
            elif s[i]=="(" or compare(s[i],opt[-1]):opt.append(s[i])#左括号或比栈顶优先级高,入
            else: #优先级不比栈顶高,opt出栈同时data出栈并计算,计算结果入data
                while opt and not compare(s[i], opt[-1]):
                    if opt[-1] == "(":break  #遇到左括号,停止计算
                    process(data, opt)
                opt.append(s[i])
            i += 1  #索引后移
        while opt:
            process(data, opt)
        print(data.pop())

    借汉诺塔理解栈与递归

    我们先说,在一个函数中,调用另一个函数。

    首先,要意识到,函数中的代码和平常所写代码一样,也都是要执行完的,只有执行完代码,或者遇到return,才会停止。

    那么,我们在函数中调用函数,执行完了,就会重新回到本函数中,继续向下执行,直到结束。

    在执行其它函数时,本函数相当于中断了,不执行了。那我们重新回来的时候,要从刚才暂停的地方开始,继续执行,这期间,所有现场信息都要原封不动,就相当于时间暂停了一样,什么都不能改变,这样才能做到程序的准确。

    所以,通常,在执行另一个函数之前,电脑会将现场信息压入一个系统栈,为被调用的函数分配存储区,然后开始执行被调函数。执行完毕后,保存计算结果,释放被调函数的空间,按照被调函数里保存的返回地址,返回到原函数。

    那什么是递归函数呢?

    就是多个函数嵌套调用。不同的是,这些函数是同一个函数,只是参数可能不同,甚至参数也一样,只是存储空间不同。

    每一层递归所需信息构成一个栈,每一块内存储着所有实在参数,所有局部变量,上一层的返回地址,等等一切现场信息。每执行完就弹出。

    递归函数有着广泛应用,主要适合可以把自身分化成一样的子问题的问题。比如汉诺塔。

     

    汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

    思路:函数(n,a,b,c)含义是把n个盘子从a柱子搬到c柱子的方法

    一个盘子,直接搬过去。

    多个盘子,我们把n-1个盘子都移动到另一个柱子上,把最大的搬过去然后把剩下的搬过去。

     

    def hanoi(n, a, b, c):
        if n == 1:
            print(a, '-->', c)
        else:
            hanoi(n - 1, a, c, b)
            print(a, '-->', c)
            hanoi(n - 1, b, a, c)
    # 调用
    hanoi(3, 'A', 'B', 'C')

    结果:

    A --> C
    A --> B
    C --> B
    A --> C
    B --> A
    B --> C
    A --> C

    我们的栈:

    第一次:

    我们把hanoi(3, 'A', 'B', 'C')存了起来,调用了hanoi(3-1, 'A', 'C', 'B'),现在栈里压入了3, 'A', 'B', 'C',还有函数执行到的位置等现场信息。然后执行hanoi(3-1, 'A', 'C', 'B'),发现要调用hanoi(3-1-1, 'A', 'B', 'C'),我们又把3-1, 'A', 'C', 'B'等信息压入了栈,现在栈是这样的:

    栈头

    2, 'A', 'C', 'B'

    3, 'A', 'B', 'C'

    栈尾

     

    然后执行hanoi(3-1-1, 'A', 'B', 'C'),发现n=1了,打印了第一条A --> C,然后释放掉了hanoi(3-1-1, 'A', 'B', 'C')的空间,并通过记录的返址回到了hanoi(3-1, 'A', 'C', 'B'),然后执行打印语句A --> B,然后发现要调用hanoi(3-1-1, 'C', 'A', 'B'),此时栈又成了:

    2, 'A', 'C', 'B'
    3, 'A', 'B', 'C'

    调用hanoi(1, 'A', 'C', 'B')发现可以直接打印,C --> B。

    然后我们又回到了2, 'A', 'C', 'B'这里。发现整个函数执行完了,那就弹出吧。这时栈是这样的:

    3, 'A', 'B', 'C'

    只有这一个。

    我们继续执行这个函数的代码,发现

    def hanoi(n, a, b, c):
        if n == 1:
            print(a, '-->', c)
        else:
            hanoi(n - 1, a, c, b)//执行到了这里
            print(a, '-->', c)
            hanoi(n - 1, b, a, c)

     

    那我们就继续执行,发现要打印A --> C

    然后继续,发现要调用        hanoi(n - 1, b, a, c),那我们继续把现场信息压栈,继续执行就好了。

     

    递归就是把大问题分解成小问题进而求解。

    具体执行就是通过系统的栈来实现返回原函数的功能。

     转存失败重新上传取消 

     

    多色汉诺塔问题:

     

    奇数号圆盘着蓝色,偶数号圆盘着红色,如图所示。现要求将塔座A 上的这一叠圆盘移到塔座B 上,并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则:

    规则(1):每次只能移动1 个圆盘;
    规则(2):任何时刻都不允许将较大的圆盘压在较小的圆盘之上;
    规则(3):任何时刻都不允许将同色圆盘叠在一起;
     

    其实双色的汉诺塔就是和无色的汉诺塔算法类似,通过推理可知,无色汉诺塔的移动规则在双色汉诺塔这里的移动规则并没有违反。

    这里说明第一种就可以了:Hanoi(n-1,A,C,B);

    在移动过程中,塔座上的最低圆盘的编号与n-1具有相同奇偶性,塔座b上的最低圆盘的编号与n-1具有不相同的奇偶性,从而塔座上的最低圆盘的编号与n具有相同的奇偶性,塔座上c最低圆盘的编号与n具有不同的奇偶性;
     

    所以把打印操作换成两个打印即可

     

    总:因为递归可能会有重复子问题的出现。

    就算写的很好,无重复子问题,也会因为来回调用、返回函数,而速度较慢。所以,有能力的可以改为迭代或动态规划等方法。

     

    单调栈

    通过使用栈这个简单的结构,我们可以巧妙地降低一些问题的时间复杂度。

    单调栈性质:

    1、若是单调递增栈,则从栈顶到栈底的元素是严格递增的。若是单调递减栈,则从栈顶到栈底的元素是严格递减的。

    2、越靠近栈顶的元素越后进栈。(显而易见)

    本文介绍单调栈用法

    通过一道题来说明。

    POJ2559

    1. 题目大意:链接

    给出一个柱形统计图(histogram), 它的每个项目的宽度是1, 高度和具体问题有关。 现在编程求出在这个柱形图中的最大面积的长方形。

    7 2 1 4 5 1 3 3

    7表示柱形图有7个数据,分别是 2 1 4 5 1 3 3, 对应的柱形图如下,最后求出来的面积最大的图如右图所示。

    做法1:枚举每个起点和终点,矩形面积就是长*最小高度。O(N^3)

    做法2:区间最小值优化。O(N^2)

    做法3:以每一个下标为中心向两边扩,遇到更短的就停,这样我们可以确定以每一个下标高度为最高的矩形。O(N^2)

    单调栈:维护一个单调递增栈,所有元素各进栈和出栈一次即可。每个元素出栈的时候更新最大的矩形面积。

    过程:

    1)判断当前元素小于栈顶

    2)条件满足,就可以更新栈顶元素的最大长度了,并且把栈顶弹出

    3)继续执行(1),直到条件不满足。

     

    重要结论:

    1)栈顶下面一个元素一定是,栈顶左边第一个比栈顶小的元素

    2)当前元素一定是,右边第一个比栈顶小的元素。

    为什么呢?

    比如这是个栈

    1)如果右边存在距离更近的比1号小的数,1号早已经弹出了。

    2)如果左边有距离更近的比1号小的数

                    如果它比2号小,它会把2号弹出,自己成为2号

                     如果它比2号大,它不会弹出2号,但是它会压栈,变成2号,原来的2号成为3号。

    所以不管怎么说,这个逻辑是正确的。

    最后放代码并讲解

     

    下面看一道难一些的题

    LeetCode 85 Maximal Rectangle

    1 0 1 0 0

    1 0 1 1 1

    1 1 1 1 1

    1 0 0 1 0

    Return 6.二三行后面那六个1

     

    给定一个由二进制组成的矩阵map,找到仅仅包含1的最大矩形,并返回其面积。

    这道题是一行一行的做。对每一行都求出每个元素对应的高度,这个高度就是对应的连续1的长度,然后对每一行都更新一次最大矩形面积。

    连续1长度也很好更新,本个元素是0,长度就是0,本个元素是1,那就加上之前的。

    具体思路代码中讲解。

    import java.util.Stack;
    
    public class MaximalRectangle {
    
    	public static int maxRecSize(int[][] map) {
    		if (map == null || map.length == 0 || map[0].length == 0) {
    			return 0;
    		}
    		int maxArea = 0;
    		int[] height = new int[map[0].length];
    		for (int i = 0; i < map.length; i++) {
    			for (int j = 0; j < map[0].length; j++) {
    				height[j] = map[i][j] == 0 ? 0 : height[j] + 1;//0长度为0,1长度为前面+1
    			}
    			maxArea = Math.max(maxRecFromBottom(height), maxArea);//调用第一题的思想
    		}
    		return maxArea;
    	}
    
    	//第一题思路
    	public static int maxRecFromBottom(int[] height) {
    		if (height == null || height.length == 0) {
    			return 0;
    		}
    		int maxArea = 0;
    		Stack<Integer> stack = new Stack<Integer>();
    		for (int i = 0; i < height.length; i++) {
                    //栈非空并且栈顶大
    			while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
    				int j = stack.pop();//弹出
    				int k = stack.isEmpty() ? -1 : stack.peek();
    				int curArea = (i - k - 1) * height[j];//计算最大
    				maxArea = Math.max(maxArea, curArea);//更新总体最大
    			}
    			stack.push(i);//直到栈顶小,压入新元素
    		}
    		//最后栈非空,右边没有更小元素使它们弹出
    		while (!stack.isEmpty()) {
    			int j = stack.pop();
    			int k = stack.isEmpty() ? -1 : stack.peek();
    			int curArea = (height.length - k - 1) * height[j];
    			maxArea = Math.max(maxArea, curArea);
    		}
    		return maxArea;
    	}
    
    	public static void main(String[] args) {
    		int[][] map = { { 1, 0, 1, 1 }, { 1, 1, 1, 1 }, { 1, 1, 1, 0 }, };
    		System.out.println(maxRecSize(map));
    	}
    
    }

     

    双端单调队列

     

    这次介绍一种新的数据结构:双端队列:双端队列是指允许两端都可以进行入队和出队操作的队列,其元素的逻辑结构仍是线性结构。将队列的两端分别称为前端和后端,两端都可以入队和出队。

    堆栈、队列和优先队列都可以采用双端队列来实现

    本文介绍单调双端队列的原理及应用。

    单调队列,顾名思义,就是一个元素单调的队列,那么就能保证队首的元素是最小(最大)的,从而满足最优性问题的需求。

    给定一个长度为n的数列,一个k,求所有的min(ai,ai+1.....ai+k-1),i=0,1,....n-k

    通俗一点说就是一个长度固定的滑动的窗口,求每个窗口内的最小值。

    你当然可以暴力求解,依次遍历每个窗口.

    介绍单调队列用法:我们维护一个单调队列

    单调队列呢,以单调递增序列为例:

    1、如果队列的长度一定,先判断队首元素是否在规定范围内,如果超范围则增长队首。

    2、每次加入元素时和队尾比较,如果当前元素小于队尾且队列非空,则减小尾指针,队尾元素依次出队,直到满足队列的调性为止

     

    我们说算法的优化就是重复计算过程的去除。

    按窗口一次次遍历就是重复计算。最值信息没有利用好。

    我们为什么可以这么维护?

    首先,遍历到的元素肯定在队列元素之后。

    其次,如果当前元素更小的话。

    头部的值比当前元素大,头部还比当前元素先过期。所以以后计算再也不会用到它了。我们可以放心的去掉它。

    下面给出代码和解释

    int n,k;//长度为n的数列,窗口为k
    int a[MAX_N];//数列
    int b[MAX_N];//存放
    int deq[MAX_N]//模拟队列
    
    void solve()
    {
        int s = 0,t = 0;//头和尾
        for(int i=0;i<n;i++)
        {
            //不满足单调,尾就弹出
            while(s<t && a[deq[t-1]]>=a[i])t--;
            //直到满足,放入
            deq[t++]=i;
            //计算窗口最大值
            if(i-k+1>=0)b[i-k+1]=a[deq[s];
            //判断头过期弹出
            if(deq[s]==i-k+1)s++;
        }
    }

    基本入门就到这里。

     单调队列优化的背包问题

    对于背包问题,经典的背包九讲已经讲的很明白了,本来就不打算写这方面问题了。

    但是吧。

    我发现,那个最出名的九讲竟然没写队列优化的背包。。。。

    那我必须写一下咯嘿嘿,这么好的思想。

     

    我们回顾一下背包问题吧。

     

    01背包问题 


    题目 
    有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

    这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 

    f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

    f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。 

    就是说,对于本物品,我们选择拿或不拿

    比如费用是3.

    相关图解:

    我们求表格中黄色部分,只和两个黑色部分有关

    拿了,背包容量减少,我们价值加上减少后最大价值。

    不拿,最大价值等于没有这件物品,背包不变,的最大价值。

    完全背包问题 


    题目 
    有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 


    基本思路 
    这个问题非常类似于01背包问题,所不同的是每种物品有无限件。

    f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]}

    图解:

    因为我们拿了本物品还可以继续拿无限件,对于当前物品,无论之前拿没拿,还可以继续拿,所以是f[i][v-c[i]]+w[i]

     

    换一个角度说明这个问题为什么可以f[i][v-c[i]]+w[i],也就是同一排。

    其实是这样的,我们对于黄色部分,也就是当前物品,有很多种选择,可以拿一个,两个。。。一直到背包容量不够了。

    也就是说,可以不拿,也就是J1,可以拿一个,也就是G1+w[i],也可以拿两个,也就是D1+2w[i],拿三个,A1+3w[i]。

    但是我们看G2,G2其实已经是之前的最大了:A1+2w[i],D1+w[i],G1他们中最大的,对么?

    既然G2是他们中最大的。

    我们怎么求J2?

    是不是只要求G2+w[i]和J1的最大值就好了。

    因为G2把剩下的情况都保存好了。

     

    多重背包问题 


    题目 
    有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

     

    和之前的完全背包不同,这次,每件物品有最多拿n[i]件的限制。

    思路一:我们可以把物品全都看成01背包:比如第i件,我们把它拆成n[i]件一样的单独物品即可。

    思路二:思路一时间复杂度太高。利用二进制思路:一个n位二进制,能表示2^n种状态,如果这些状态就是拿了多少物品,我们可以把每一位代表的数都拿出来,比如n[i]=16,我们把它拆成1,2,4,8,1,每一堆物品看成一个单独物品。

    为什么最后有个一?因为从0到16有十七种状态,四位不足以表示。我们最后补上第五位1.

    把拆出来的物品按01背包做即可。

    思路三:我们可以利用单调队列:

    https://blog.csdn.net/hebtu666/article/details/82720880

    再回想完全背包:为什么可以那么做?因为每件物品能拿无限件。所以可以。而多重背包因为有了最多拿多少的限制,我们就不敢直接从G2中拿数,因为G2可能是拿满了本物品以后才达到的状态 。

    比如n[i]=2,如果G2的状态是2w[i],拿了两个2物品达到最大值,我们的J2就不能再拿本物品了。

    如何解决这个问题?就是我给的网址中的,双端单调队列

    利用窗口最大值的思想。

    大家想想怎么实现再看下文。

     

    发现问题了吗?

    我们求出J2以后,按原来的做法,是该求K2的,但是K2所需要的信息和J2完全不同,红色才是K2可能需要的信息。

    所以我们以物品重量为差,先把黑色系列推出来,再推红色系列,依此类推。

    这个例子就是推三次,每组各元素之间差3.

    这样就不会出现构造一堆单调队列的尴尬情况了。

    在代码中继续详细解释:

    //输入
    int n;
    int W;
    int w[MAX_N];
    int v[MAX_N];
    int m[MAX_N];

     

    int dp[MAX_N+1];//压空间,本知识参考https://blog.csdn.net/hebtu666/article/details/79964233
    int deq[MAX_N+1];//双端队列,保存下标
    int deqv[MAX_N+1];//双端队列,保存值

    队列存的就是所有上一行能取到的范围,比如对于J2,队列里存的就是G1-w[i],D1-2w[i],A1-3w[i]等等合法情况。(为了操作方便都是j,利用差实现最终的运算)

    他们之中最大的就是队头,加上最多存储个数就好。

     

     

     

    void solve()
    {
        for(int i=0;i<n;i++)//参考过那个网址第二题应该懂
        {
            for(int a=0;a<w[i];a++)//把每个分组都打一遍
            {
                int s=0;//初始化双端队列头尾
                int t=0;
                for(int j=0;j*w[i]+a<=W;j++)//每组第j个元素
                {
                    int val=dp[j*w[i]+a]-j*v[i];
                    while(s<t && deqv[t-1]<=val)//直到不改变单调性
                        t--;
                    deq[t]=j;
                    deqv[t]=val;
                    t++;
                    //利用队头求出dp
                    dp[j*w[i]+a]=deqv[s]+j*v[i];
                    if(deq[s]==j-m[i])s++;//检查过期
                }
            }
        }
    }

     串的定长表示

    思想和代码都不难,和线性表也差不多,串本来就是数据受限的线性表。

    串连接:

     

    #include <stdio.h>
    #include <string.h>
    //串的定长顺序存储表示
    #define MAXSTRLEN 255							//用户可在255以内定义最大串长
    typedef unsigned char SString[MAXSTRLEN + 1];	//0号单元存放串的长度
    
    int Concat(SString *T,SString S1,SString S2)
    	//用T返回S1和S2联接而成的新串。若未截断返回1,若截断返回0
    {
    	int i = 1,j,uncut = 0;
    	if(S1[0] + S2[0] <= MAXSTRLEN)	//未截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    		for (j = 1; j <= S2[0]; j++)
    			(*T)[S1[0]+j] = S2[j];	//(*T)[i+j] = S2[j]
    		(*T)[0] = S1[0] + S2[0];
    		uncut = 1;
    	}
    	else if(S1[0] < MAXSTRLEN)		//截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    
    		for (j = S1[0] + 1; j <= MAXSTRLEN; j++)
    		{
    			(*T)[j] = S2[j - S1[0] ];
    			(*T)[0] = MAXSTRLEN;
    			uncut = 0;
    		}
    	}
    	else
    	{
    		for (i = 0; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		/*或者分开赋值,先赋值内容,再赋值长度
    		for (i = 1; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		(*T)[0] = MAXSTRLEN;
    		*/
    		uncut = 0;
    	}
    
    	return uncut;
    }
    
    int SubString(SString *Sub,SString S,int pos,int len)
    	//用Sub返回串S的第pos个字符起长度为len的子串
    	//其中,1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S) - pos + 1(从pos开始到最后有多少字符)
    	//第1个字符的下标为1,因为第0个字符存放字符长度
    {
    	int i;
    	if(pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)
    		return 0;
    	for (i = 1; i <= len; i++)
    	{
    		//S中的[pos,len]的元素 -> *Sub中的[1,len]
    		(*Sub)[i] = S[pos + i - 1];//下标运算符 > 寻址运算符的优先级
    	}
    	(*Sub)[0] = len;
    	return 1;
    }
    void PrintStr(SString S)
    {
    	int i;
    	for (i = 1; i <= S[0]; i++)
    		printf("%c",S[i]);
    	printf("\n");
    }
    
    
    int main(void)
    {
    	/*定长顺序存储初始化和打印的方法
    	SString s = {4,'a','b','c','d'};
    	int i;
    	//s = "abc";	//不可直接赋值
    	for (i = 1; i <= s[0]; i++)
    		printf("%c",s[i]);
    	*/
    	SString s1 = {4,'a','b','c','d'};
    	SString s2 = {4,'e','f','g','h'},s3;
    	SString T,Sub;
    	int i;
    	
    	for (i = 1; i <= 255; i++)
    	{
    		s3[i] = 'a';
    		if(i >= 248)
    			s3[i] = 'K';
    	}
    	s3[0] = 255;
    	SubString(&Sub,s3,247,8);
    	PrintStr(Sub);
    	
    
    
    
    	return 0;
    }

    串的堆分配实现

     

    今天,线性结构基本就这样了,以后(至少是最近)就很少写线性基础结构的实现了。

    串的类型定义

    typedef struct
    {
        char *str;
        int length;
    }HeapString;


    初始化串

    InitString(HeapString *S)
    {
        S->length=0;
        S->str='\0';
    }

    长度

    int StrEmpty(HeapString S)
    /*判断串是否为空,串为空返回1,否则返回0*/
    {
        if(S.length==0)         /*判断串的长度是否等于0*/
            return 1;           /*当串为空时,返回1;否则返回0*/
        else
            return 0;
    }
    int StrLength(HeapString S)
    /*求串的长度操作*/
    {
        return S.length;
    }


    串的赋值

    void StrAssign(HeapString *S,char cstr[])
    /*串的赋值操作*/
    {
        int i=0,len;
        if(S->str)
            free(S->str);
        for(i=0;cstr[i]!='\0';i++); /*求cstr字符串的长度*/
            len=i;
        if(!i)
        {
            S->str=NULL;
            S->length=0;
        }
        else
        {
            S->str=(char*)malloc((len+1)*sizeof(char));
            if(!S->str)
                exit(-1);
            for(i=0;i<len;i++)
                S->str[i]=cstr[i];
    
            S->length=len;
        }
    }


    串的复制

    void StrAssign(HeapString *S,char cstr[])
    /*串的赋值操作*/
    {
        int i=0,len;
        if(S->str)
            free(S->str);
        for(i=0;cstr[i]!='\0';i++); /*求cstr字符串的长度*/
            len=i;
        if(!i)
        {
            S->str=NULL;
            S->length=0;
        }
        else
        {
            S->str=(char*)malloc((len+1)*sizeof(char));
            if(!S->str)
                exit(-1);
            for(i=0;i<len;i++)
                S->str[i]=cstr[i];
    
            S->length=len;
        }
    }


    串的插入

    int StrInsert(HeapString *S,int pos,HeapString T)
    /*串的插入操作。在S中第pos个位置插入T分为三种情况*/
    {
        int i;
        if(pos<0||pos-1>S->length)      /*插入位置不正确,返回0*/
        {
            printf("插入位置不正确");
            return 0;
        }
        S->str=(char*)realloc(S->str,(S->length+T.length)*sizeof(char));
        if(!S->str)
        {
            printf("内存分配失败");
            exit(-1);
        }
    
        for(i=S->length-1;i>=pos-1;i--)
            S->str[i+T.length]=S->str[i];
        for(i=0;i<T.length;i++)
            S->str[pos+i-1]=T.str[i];
    
        S->length=S->length+T.length;
        return 1;
    }


    串的删除

    int StrDelete(HeapString *S,int pos,int len)
    /*在串S中删除pos开始的len个字符*/
    {
        int i;
        char *p;
        if(pos<0||len<0||pos+len-1>S->length)
        {
            printf("删除位置不正确,参数len不合法");
            return 0;
        }
        p=(char*)malloc(S->length-len);             /*p指向动态分配的内存单元*/
        if(!p)
            exit(-1);
        for(i=0;i<pos-1;i++)                        /*将串第pos位置之前的字符复制到p中*/
            p[i]=S->str[i];
        for(i=pos-1;i<S->length-len;i++)                /*将串第pos+len位置以后的字符复制到p中*/
            p[i]=S->str[i+len];
        S->length=S->length-len;                    /*修改串的长度*/
        free(S->str);                           /*释放原来的串S的内存空间*/
        S->str=p;                               /*将串的str指向p字符串*/
        return 1;
    }



    串的比较

    int StrCompare(HeapString S,HeapString T)
    /*串的比较操作*/
    {
    int i;
    for(i=0;i<S.length&&i<T.length;i++) /*比较两个串中的字符*/
        if(S.str[i]!=T.str[i])          /*如果出现字符不同,则返回两个字符的差值*/
            return (S.str[i]-T.str[i]);
    return (S.length-T.length);             /*如果比较完毕,返回两个串的长度的差值*/
    }


    串的连接

    int StrCat(HeapString *T,HeapString S)
    /*将串S连接在串T的后面*/
    {
        int i;
        T->str=(char*)realloc(T->str,(T->length+S.length)*sizeof(char));
        if(!T->str)
        {
            printf("分配空间失败");
            exit(-1);
        }
        else
        {
            for(i=T->length;i<T->length+S.length;i++)   /*串S直接连接在T的末尾*/
                T->str[i]=S.str[i-T->length];
            T->length=T->length+S.length;           /*修改串T的长度*/
        }
        return 1;
    }


    清空串

    void StrClear(HeapString *S)
    /*清空串,只需要将串的长度置为0即可*/
    {
    
        S->str='\0';
        S->length=0;
    }


    销毁串

    void StrDestroy(HeapString *S)
    {
        if(S->str)
            free(S->str);
    }

    打印

    void StrPrint(HeapString S)
    {
        int i;
        for(i=0;i<S.length;i++)
        {
            printf("%c",S.str[i]);
        }
        printf("\n");
    }

    KMP

    Kmp操作、原理、拓展

     

     

    注:虽然我是一只菜,才大一。但我是想让萌新们更容易的学会一些算法和思想,所以没有什么专业词语,用的都是比较直白地表达,大佬们可能觉得烦,但是真的对不会的人更有帮助啊。我本人也是菜,大一上学期写的,直接拿过来了,也没检查,有什么错误大佬们赶紧告诉我

    先上代码,大佬们可以别看下面了,就当复习一下

    package advanced_001;
    
    public class Code_KMP {
    
    	public static int getIndexOf(String s, String m) {
    		if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
    			return -1;
    		}
    		char[] str1 = s.toCharArray();
    		char[] str2 = m.toCharArray();
    		int i1 = 0;
    		int i2 = 0;
    		int[] next = getNextArray(str2);
    		while (i1 < str1.length && i2 < str2.length) {
    			if (str1[i1] == str2[i2]) {
    				i1++;
    				i2++;
    			} else if (next[i2] == -1) {
    				i1++;
    			} else {
    				i2 = next[i2];
    			}
    		}
    		return i2 == str2.length ? i1 - i2 : -1;
    	}
    
    	public static int[] getNextArray(char[] ms) {
    		if (ms.length == 1) {
    			return new int[] { -1 };
    		}
    		int[] next = new int[ms.length];
    		next[0] = -1;
    		next[1] = 0;
    		int i = 2;
    		int cn = 0;
    		while (i < next.length) {
    			if (ms[i - 1] == ms[cn]) {
    				next[i++] = ++cn;
    			} else if (cn > 0) {
    				cn = next[cn];
    			} else {
    				next[i++] = 0;
    			}
    		}
    		return next;
    	}
    
    	public static void main(String[] args) {
    		String str = "abcabcababaccc";
    		String match = "ababa";
    		System.out.println(getIndexOf(str, match));
    
    	}
    
    }
    

     

    问题:给定主串S和子串 T,如果在主串S中能够找到子串 T,则匹配成功,返回第一个和子串 T 中第一个字符相等的字符在主串S中的序号;否则,称匹配失败,返回 0。

     

    一、引子

    原始算法:以主串中每一个位置为开头,与子串第一个元素匹配,若相同,下一个位置和子串下一个位置匹配,如果子串元素全部匹配成功,则匹配成功,找到位置。

    非常傻白甜,很明显时间复杂度最差为o(len(s)*len(t))。效率很低,大佬请忽略:

     

    引出KMP算法,概念如下:KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是实现一个next()函数,函数本身包含了模式串的局部匹配信息。时间复杂度O(m+n)。(摘自百度百科)

     

     

    其实就是说,人家kmp算法时间复杂度o(len(s)+len(t)),非常快了,毕竟你不遍历一遍这俩字符串,怎么可能匹配出来呢?我不信还有时间复杂度更低的算法,包括优化也是常数范围的优化,时间已经非常优秀了

    二、分析总结

    分析:首先,我们要搞明白,原始的算法为啥这么慢呢?因为它在一遍一遍的遍历s和t,做了很多重复工作,浪费了一些我们本该知道的信息。大大降低了效率。

    比如t长度为10,s匹配到位置5,如果t一直匹配到了t[8],到[9]才匹配错误,那s已经匹配到了位置14,下一步怎么做呢?接着从位置6开始,和t[0]开始匹配,而s位置6和t[0]甚至后面很大一部分信息我们其实都遍历过,都知道了,原始算法却还要重复匹配这些位置。所以效率极低。

    (其实很多算法都是对一般方法中的重复运算、操作做了优化,当我们写出暴力递归后,应分析出我们做了哪些重复运算,然后优化。具体优化思路我会在以后写出来。当我们可以用少量的空间就能减少大量的时间时,何乐而不为呢?)

    扯远了,下面开始进入kmp正题。

    三、基本操作

    首先扯基本操作:

    next数组概念:一个字符串中最长的相同前后缀的长度,加一。可能表达的不太好啊,举例说明:abcabcabc

    所以next[1]一直到next[9]计算的是a,ab,abc,abca,abcab直到abcabcabc的相同的最长前缀和最长后缀,加一

    注意,所谓前缀,不能包含最后一个字符,而后缀,也不能包含第一个字符,如果包含,那所有的next都成了字符串长度,也就没意义了。

    比如a,最长前后缀长度为0,原因上面刚说了,不包含。

    abca最长前后缀长度为1,即第一个和最后一个。

    abcab最长前后缀长度为2,即ab

    abcabc最长前后缀长度为3,即abc

    abcabca最长前后缀长度为4,即abca

    abcabcabc最长前后缀长度为6,即abcabc

    萌新可以把next数组看成一个黑盒,我下面会写怎么求,不过现在先继续讲主体思路。

    (感觉next数组体现了一个挺重要的思想:预处理思想。当我们不能直接求解问题时,不妨先生成一个预处理的数组,用来记录我们需要的一些信息。以后我会写这方面的专题)

     

     

     

     

    开始匹配了哦:假如主串从i位置开始和子串配,配到了i+j时配不下去了,按原来的方法,应该回到i+1,继续配,而kmp算法是这样的:

    黑色部分就是配到目前为止,前面子串中的最长相同前后缀。匹配失败以后,可以跳过我圈的那一部分开头,从主串的第二块黑色那里开始配了,这些开头肯定配不出来,这就是kmp核心的思想,至于为什么敢跳,等会讲,现在先说基本操作。

    根据定义,主串第二块黑部分和子串第一块黑部分也一样,所以直接从我划线的地方往下配就好。

    就这样操作,直到最后或配出。

     

    四、原理

    原始的kmp操作就是这样,下面讲解原理,为什么能确定跳过的那一段开头肯定配不出来呢?

    还是再画一个图来配合讲解吧。(要不然我怕表达不好唉。。好气哟)

    (懒,就是刚才的图改了改)

    咱们看普遍情况(注意,是普遍情况,任意跳过的开头位置),随便一个咱们跳过的开头,看看有没有可能配出来呢?

    竖线叫abc吧。

    主串叫s,子串交t

    请看ab线中间包含的t中的子串,它在t中是一个以t[0]为开头,比黑块更长的前缀。

    请看ab线中间包含的s中的子串,它在s中是一个以b线前一个元素为结尾,比黑块更长的后缀。

    请回想黑块定义:这是目前位置之前的子串中,最长的相同前后缀。

    请再想一想我们当初为什么能配到这里呢?

     

    这个位置之前,我们全都一样,所以多长的后缀都是相等的。

    其实就是,主数组后缀等于子数组后缀,而子数组前缀不等于子数组后缀,所以子数组前缀肯定不等与主数组后缀,也就是说,当前位置肯定配不出来

     

    这是比最长相同前后缀更长的前后缀啊兄弟。。。所以肯定不相等,如果相等,最长相同前后缀至少也是它了啊,对么?这就是能跳过的原因,这辈子不可能在这里面配出来了哦。

    主要操作和原理就这些了。。不知道解释清楚没。

    下面解释如何求解next数组:

     

    当然,一个一个看也不是不可以,在子串很短的情况下算法总时间区别不大,但是。。各位有没有一股似曾相识的感觉呢?计算next[x]还是要在t[0]-t[x-2]这个串里找最大相同前后缀啊。还是串匹配问题啊。看操作:

    (一切为了code简洁好操作),之后每个位置看看p[i-1]和p[next[i-1]]是不是相等,请回去看图,也就是第一个黑块后面那个元素和第二个黑块最后那个元素,相等,next[i]就等于next[i-1]+1。(求b,看前一个元素的最长前后缀,前一个元素和a看是不是相等。)

    若不等,继续往前看,p[i-1]是不是等于p[next[next[i-1]]],就这样一直往前跳。其实现在一看,大家是不是感觉,和s与t匹配的时候kmp主体很像啊?只是反过来跳了嘛。。。原理也是基本一样的,我就不解释了,跳过的部分也不可能配出来,你们自己证吧,不想写了。

     

    五、复杂度分析

    下面分析时间复杂度:

    主体部分,在主串上的指针,两种情况,要么配了头一个就不对,就往后走了,这时用o(1)排除了一个位置。要么就是,配了n个位置以后配不对了,那不管next数组是多少,主串上的指针总会向后走n个位置的,所以每个位置还是o(1),这样看来,主串长度是len的话,时间复杂度就是o(len)啊。

    再看next数组求解的操作,一样的啊,最多就是子串的长度那么多呗。

    所以总体时间复杂度o(m+n),原来是o(m*n)啊,向这位大神致敬,想出这么强的算法。

    六、kmp拓展题目

    (本来想放到树专题讲,但是kmp提供了很好的思路,故在本章讲述kmp方法,在树专题讲一般思路)

    输入两棵二叉树A,B,判断B是不是A的子结构。

    Oj链接

    https://www.nowcoder.com/practice/6e196c44c7004d15b1610b9afca8bd88?tpId=13&tqId=11170&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

    先说一般思路,就一个一个试呗,先在A里找B的根,相等了接着往下配,全配上就行了。

    需要注意的是,子结构的定义,好好理解,不要搞错了,不太清楚定义的自己查资料。

     

    下面说利用kmp解此题的思路

    Kmp,解决字符串匹配问题,而此题是二叉树匹配问题,所以一般思路是想把树序列化,然后用kmp,但是我们有一个常识,一种遍历不能确定唯一一颗树,这是我们首先要解决的问题。

    分析为什么一个序列不能确定呢?给你个序列建立二叉树,比如1 2 3,先序吧(默认先左子树),1是根没问题,2就不一定了,可以是左子树可以是右子树,假如是左子树,那三可放的位置更不确定,这就是原因,我们不知道左子树是空,结束了,该建右子树,还是说,填在左子树。

    怎么解决这个问题?

    我请教了敬爱的老师这方法对不对,所以肯定没有问题滴。

    只要把空也表示出来就好了比如最简单的例子,先序的话就生成1 2 空 空 3 空 空

    再举一例1 2 4 空 空 空 3 空 空

    在座的各位都是大佬,应该都懂吧。

    (因为序列化和重建的方式一样,知道左子树什么时候为空,所以可以确定唯一一颗结构确定的树)

    AB树序列化以后,用kmp字符串匹配就行啦

    (当然要是为了过oj,就别秀kmp操作了,直接用系统函数,面试再自己写)

     

     

     

    整篇结束,code怎么整合,如何操作、kmp的优化,以及篇中提到的算法思想怎么养成以后可能会写。

    字数3170

     

    初稿2017/12/20

     

     18/11/26添加网址和代码:

    https://blog.csdn.net/hebtu666/article/details/84553147

    public class T1SubtreeEqualsT2 {
    
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    
    	public static boolean isSubtree(Node t1, Node t2) {
    		String t1Str = serialByPre(t1);
    		String t2Str = serialByPre(t2);
    		return getIndexOf(t1Str, t2Str) != -1;
    	}
    
    	public static String serialByPre(Node head) {
    		if (head == null) {
    			return "#!";
    		}
    		String res = head.value + "!";
    		res += serialByPre(head.left);
    		res += serialByPre(head.right);
    		return res;
    	}
    
    	// KMP
    	public static int getIndexOf(String s, String m) {
    		if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
    			return -1;
    		}
    		char[] ss = s.toCharArray();
    		char[] ms = m.toCharArray();
    		int[] nextArr = getNextArray(ms);
    		int index = 0;
    		int mi = 0;
    		while (index < ss.length && mi < ms.length) {
    			if (ss[index] == ms[mi]) {
    				index++;
    				mi++;
    			} else if (nextArr[mi] == -1) {
    				index++;
    			} else {
    				mi = nextArr[mi];
    			}
    		}
    		return mi == ms.length ? index - mi : -1;
    	}
    
    	public static int[] getNextArray(char[] ms) {
    		if (ms.length == 1) {
    			return new int[] { -1 };
    		}
    		int[] nextArr = new int[ms.length];
    		nextArr[0] = -1;
    		nextArr[1] = 0;
    		int pos = 2;
    		int cn = 0;
    		while (pos < nextArr.length) {
    			if (ms[pos - 1] == ms[cn]) {
    				nextArr[pos++] = ++cn;
    			} else if (cn > 0) {
    				cn = nextArr[cn];
    			} else {
    				nextArr[pos++] = 0;
    			}
    		}
    		return nextArr;
    	}
    
    	public static void main(String[] args) {
    		Node t1 = new Node(1);
    		t1.left = new Node(2);
    		t1.right = new Node(3);
    		t1.left.left = new Node(4);
    		t1.left.right = new Node(5);
    		t1.right.left = new Node(6);
    		t1.right.right = new Node(7);
    		t1.left.left.right = new Node(8);
    		t1.left.right.left = new Node(9);
    
    		Node t2 = new Node(2);
    		t2.left = new Node(4);
    		t2.left.right = new Node(8);
    		t2.right = new Node(5);
    		t2.right.left = new Node(9);
    
    		System.out.println(isSubtree(t1, t2));
    
    	}
    
    }
    

     

    Manacher

    Manacher's Algorithm 马拉车算法操作及原理 

    package advanced_001;
    
    public class Code_Manacher {
    
    	public static char[] manacherString(String str) {
    		char[] charArr = str.toCharArray();
    		char[] res = new char[str.length() * 2 + 1];
    		int index = 0;
    		for (int i = 0; i != res.length; i++) {
    			res[i] = (i & 1) == 0 ? '#' : charArr[index++];
    		}
    		return res;
    	}
    
    	public static int maxLcpsLength(String str) {
    		if (str == null || str.length() == 0) {
    			return 0;
    		}
    		char[] charArr = manacherString(str);
    		int[] pArr = new int[charArr.length];
    		int C = -1;
    		int R = -1;
    		int max = Integer.MIN_VALUE;
    		for (int i = 0; i != charArr.length; i++) {
    			pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
    			while (i + pArr[i] < charArr.length && i - pArr[i] > -1) {
    				if (charArr[i + pArr[i]] == charArr[i - pArr[i]])
    					pArr[i]++;
    				else {
    					break;
    				}
    			}
    			if (i + pArr[i] > R) {
    				R = i + pArr[i];
    				C = i;
    			}
    			max = Math.max(max, pArr[i]);
    		}
    		return max - 1;
    	}
    
    	public static void main(String[] args) {
    		String str1 = "abc1234321ab";
    		System.out.println(maxLcpsLength(str1));
    	}
    
    }
    

    问题:查找一个字符串的最长回文子串

    首先叙述什么是回文子串:回文:就是对称的字符串,或者说是正反一样的

    小问题一:请问,子串和子序列一样么?请思考一下再往下看

     当然,不一样。子序列可以不连续,子串必须连续。

    举个例子,123的子串包括1,2,3,12,23,123(一个字符串本身是自己的最长子串),而它的子序列是任意选出元素组成,他的子序列有1,2,3,12,13,23,123,””,空其实也算,但是本文主要是想叙述回文,没意义。

    小问题二:长度为n的字符串有多少个子串?多少个子序列?

     子序列,每个元素都可以选或者不选,所以有2的n次方个子序列(包括空)

    子串:以一位置开头,有n个子串,以二位置开头,有n-1个子串,以此类推,我们发现,这是一个等差数列,而等差序列求和,有n*(n+1)/2个子串(不包括空)。

    (这里有一个思想需要注意,遇到等差数列求和,基本都是o(n^2)级别的)

    一、分析枚举的效率

    好,我们来分析一下暴力枚举的时间复杂度,上文已经提到过,一个字符串的所有子串,数量是o(n^2)级别,所以光是枚举出所有情况时间就是o(n^2),每一种情况,你要判断他是不是回文的话,还需要o(n),情况数和每种情况的时间,应该乘起来,也就是说,枚举时间要o(n^3),效率太低。

    二、初步优化

    思路:我们知道,回文全是对称的,每个回文串都会有自己的对称轴,而两边都对称。我们如果从对称轴开始, 向两边阔,如果总相等,就是回文,扩到两边不相等的时候,以这个对称轴向两边扩的最长回文串就找到了。

    举例:1 2 1 2 1 2 1 1 1

    我们用每一个元素作为对称轴,向两边扩

    0位置,左边没东西,只有自己;

    1位置,判断左边右边是否相等,1=1所以接着扩,然后左边没了,所以以1位置为对称轴的最长回文长度就是3;

    2位置,左右都是2,相等,继续,左右都是1,继续,左边没了,所以最长为5

    3位置,左右开始扩,1=1,2=2,1=1,左边没了,所以长度是7

    如此把每个对称轴扩一遍,最长的就是答案,对么?

    你要是点头了。。。自己扇自己两下。

    还有偶回文呢,,比如1221,123321.这是什么情况呢?这个对称轴不是一个具体的数,因为人家是偶回文。

    问题三:怎么用对称轴向两边扩的方法找到偶回文?(容易操作的)

    我们可以在元素间加上一些符号,比如/1/2/1/2/1/2/1/1/1/,这样我们再以每个元素为对称轴扩就没问题了,每个你加进去的符号都是一个可能的偶数回文对称轴,此题可解。。。因为我们没有错过任何一个可能的对称轴,不管是奇数回文还是偶数回文。

    那么请问,加进去的符号,有什么要求么?是不是必须在原字符中没出现过?请思考

     

    其实不需要的,大家想一下,不管怎么扩,原来的永远和原来的比较,加进去的永远和加进去的比较。(不举例子说明了,自己思考一下)

    好,分析一波时间效率吧,对称轴数量为o(n)级别,每个对称轴向两边能扩多少?最多也就o(n)级别,一共长度才n; 所以n*n是o(n^2)   (最大能扩的位置其实也是两个等差数列,这么理解也是o(n^2),用到刚讲的知识)

     

    小结:

    这种方法把原来的暴力枚举o(n^3)变成了o(n^2),大家想一想为什么这样更快呢?

    我在kmp一文中就提到过,我们写出暴力枚举方法后应想一想自己做出了哪些重复计算,错过了哪些信息,然后进行优化。

    看我们的暴力方法,如果按一般的顺序枚举,012345,012判断完,接着判断0123,我是没想到可以利用前面信息的方法,因为对称轴不一样啊,右边加了一个元素,左边没加。所以刚开始,老是想找一种方法,左右都加一个元素,这样就可以对上一次的信息加以利用了。

    暴力为什么效率低?永远是因为重复计算,举个例子:12121211,下标从0开始,判断1212121是否为回文串的时候,其实21212和121等串也就判断出来了,但是我们并没有记下结果,当枚举到21212或者121时,我们依旧是重新尝试了一遍。(假设主串长度为n,对称轴越在中间,长度越小的子串,被重复尝试的越多。中间那些点甚至重复了n次左右,本来一次搞定的事)

    还是这个例子,我换一个角度叙述一下,比较直观,如果从3号开始向两边扩,121,21212,最后扩到1212121,时间复杂度o(n),用枚举的方法要多少时间?如果主串长度为n,枚举尝试的子串长度为,3,5,7....n,等差数列,大家读到这里应该都知道了,等差数列求和,o(n^2)。

    三、Manacher原理

    首先告诉大家,这个算法时间可以做到o(n),空间o(n).

    好的,开始讲解这个神奇的算法。

    首先明白两个概念:

    最右回文边界R:挺好理解,就是目前发现的回文串能延伸到的最右端的位置(一个变量解决)

    中心c:第一个取得最右回文边界的那个中心对称轴;举个例子:12121,二号元素可以扩到12121,三号元素 可以扩到121,右边界一样,我们的中心是二号元素,因为它第一个到达最右边界

    当然,我们还需要一个数组p来记录每一个可能的对称轴最后扩到了哪里。

    有了这么几个东西,我们就可以开始这个神奇的算法了。

    为了容易理解,我分了四种情况,依次讲解:

     

    假设遍历到位置i,如何操作呢

     

    1)i>R:也就是说,i以及i右边,我们根本不知道是什么,因为从来没扩到那里。那没有任何优化,直接往右暴力 扩呗。

    (下面我们做i关于c的对称点,i

    2)i<R:,

    三种情况:

    i’的回文左边界在c回文左边界的里面

    i回文左边界在整体回文的外面

    i左边界和c左边界是一个元素

    (怕你忘了概念,c是对称中心,c它当初扩到了R,R是目前扩到的最右的地方,现在咱们想以i为中心,看能扩到哪里。)

    按原来o(n^2)的方法,直接向两边暴力扩。好的,魔性的优化来了。咱们为了好理解,分情况说。首先,大家应该知道的是,i’其实有人家自己的回文长度,我们用数组p记录了每个位置的情况,所以我们可以知道以i为中心的回文串有多长。

    2-1)i’的回文左边界在c回文的里面:看图

    我用这两个括号括起来的就是这两个点向两边扩到的位置,也就是i和i’的回文串,为什么敢确定i回文只有这么长?和i一样?我们看c,其实这个图整体是一个回文串啊。

    串内完全对称(1是括号左边相邻的元素,2是右括号右边相邻的元素,34同理),

     由此得出结论1:

    由整体回文可知,点2=点3,点1=点4

     

    当初i’为什么没有继续扩下去?因为点1!=点2。

    由此得出结论2:点1!=点2 

     

    因为前面两个结论,所以3!=4,所以i也就到这里就扩不动了。而34中间肯定是回文,因为整体回文,和12中间对称。

     

    2-2)i回文左边界在整体回文的外面了:看图

    这时,我们也可以直接确定i能扩到哪里,请听分析:

    当初c的大回文,扩到R为什么就停了?因为点2!=点4----------结论1;

    2为2关于i的对称点,当初i左右为什么能继续扩呢?说明点2=点2’---------结论2;

    由c回文可知2’=3,由结论2可知点2=点2’,所以2=3;

    但是由结论一可知,点2!=点4,所以推出3!=4,所以i扩到34为止了,34不等。

    而34中间那一部分,因为c回文,和i在内部的部分一样,是回文,所以34中间部分是回文。

     

    2-3)最后一种当然是i左边界和c左边界是一个元素

    点1!=点2,点2=点3,就只能推出这些,只知道34中间肯定是回文,外边的呢?不知道啊,因为不知道3和4相不相等,所以我们得出结论:点3点4内肯定是,继续暴力扩。

    原理及操作叙述完毕,不知道我讲没讲明白。。。

    四、代码及复杂度分析

     看代码大家是不是觉得不像o(n)?其实确实是的,来分析一波。。

    首先,我们的i依次往下遍历,而R(最右边界)从来没有回退过吧?其实当我们的R到了最右边,就可以结束了。再不济i自己也能把R一个一个怼到最右

    我们看情况一和四,R都是以此判断就向右一个,移动一次需要o(1)

    我们看情况二和三,直接确定了p[i],根本不用扩,直接遍历下一个元素去了,每个元素o(1).

    综上,由于i依次向右走,而R也没有回退过,最差也就是i和R都到了最右边,而让它们移动一次的代价都是o(1)的,所以总体o(n)

    可能大家看代码依旧有点懵,其实就是code整合了一下,我们对于情况23,虽然知道了它肯定扩不动,但是我们还是给它一个起码是回文的范围,反正它扩一下就没扩动,不影响时间效率的。而情况四也一样,给它一个起码是回文,不用验证的区域,然后接着扩,四和二三的区别就是。二三我们已经心中有B树,它肯定扩不动了,而四确实需要接着尝试。

    (要是写四种情况当然也可以。。但是我懒的写,太多了。便于理解分了四种情况解释,code整合后就是这样子)

     

    字数3411

    2017/12/22

     

     

    前缀树

    是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

    字典树又称为前缀树或Trie树,是处理字符串常见的数据结构。假设组成所有单词的字符仅是“a”~"z",请实现字典树结构,并包含以下四个主要功能:

    void insert(String word):添加word,可重复添加。
    void delete(String word):删除word,如果word添加过多次,仅删除一次。
    boolean search(String word):查询word是否在字典树中。
    int prefixNumber(String pre):返回以字符串pre为前缀的单词数量。
    思考:

    字典树的介绍。字典树是一种树形结构,优点是利用字符串的公共前缀来节约存储空间。

     

    基本性质:

    字典树的基本性质如下:

    • 根节点没有字符路径。除根节点外,每一个节点都被一个字符路径找到。
    • 从根节点到某一节点,将路径上经过的字符连接起来,为扫过的对应字符串。
    • 每个节点向下所有的字符路径上的字符都不同。

    也不需要记,看了实现,很自然的性质就理解了。

    每个结点内有一个指针数组,里面有二十六个指针,分别指向二十六个字母。

    如果指向某个字母的指针为空,那就是以前没有遇到过这个前缀。

     

    搜索的方法为:

    (1) 从根结点开始一次搜索;

    (2) 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;

    (3) 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。

    (4) 迭代过程……

    (5) 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找。

    其他操作类似处理

    插入也一样,只是转到某个子树时,没有子树,那就创建一个新节点,然后对应指针指向新节点即可。

    我们给出定义就更清楚了:

    public static class TrieNode {
    	public int path; //表示由多少个字符串共用这个节点
    	public int end;//表示有多少个字符串是以这个节点结尾的
    	public TrieNode[] map;
        //哈希表结构,key代表该节点的一条字符路径,value表示字符路径指向的节点
    	public TrieNode() {
    	    path = 0;
    	    end = 0;
    	    map = new TrieNode[26];
    	}
    }

    path和end都是有用的,接下来会说明

    insert:

    	    public static class Trie {
    	        private TrieNode root;//头
    	 
    	        public Trie() {
    	            root = new TrieNode();
    	        }
    	 
    	        public void insert(String word) {
    	            if (word == null) {
    	                return;
    	            }//空串
    	            char[] chs = word.toCharArray();
    	            TrieNode node = root;
    	            int index = 0; //哪条路
    	            for (int i = 0; i < chs.length; i++) {
    	                index = chs[i] - 'a'; //0~25
    	                if (node.map[index] == null) {
    	                    node.map[index] = new TrieNode();
    	                }//创建,继续
    	                node = node.map[index];//指向子树
    	                node.path++;//经过加1
    	            }
    	            node.end++;//本单词个数加1
    	        }
    	        public boolean search(String word) {
    	            if (word == null) {
    	                return false;
    	            }
    	            char[] chs = word.toCharArray();
    	            TrieNode node = root;
    	            int index = 0;
    	            for (int i = 0; i < chs.length; i++) {
    	                index = chs[i] - 'a';
    	                if (node.map[index] == null) {
    	                    return false;//找不到
    	                }
    	                node = node.map[index];
    	            }
    	            return node.end != 0;//end标记有没有以这个字符为结尾的字符串
    	        }

    delete: 

    	        public void delete(String word) {
                      //如果有
    	            if (search(word)) {
    	                char[] chs = word.toCharArray();
    	                TrieNode node = root;
    	                int index = 0;
    	                for (int i = 0; i < chs.length; i++) {
    	                    index = chs[i] - 'a';
    	                    if (node.map[index].path-- == 1) {//path减完之后为0
    	                        node.map[index] = null;
    	                        return;
    	                    }
    	                    node = node.map[index];//去子树
    	                }
    	                node.end--;//次数减1
    	            }
    	        }

    prefixNumber:

     public int prefixNumber(String pre) {
    	            if (pre == null) {
    	                return 0;
    	            }
    	            char[] chs = pre.toCharArray();
    	            TrieNode node = root;
    	            int index = 0;
    	            for (int i = 0; i < chs.length; i++) {
    	                index = chs[i] - 'a';
    	                if (node.map[index] == null) {
    	                    return 0;//找不到
    	                }
    	                node = node.map[index];
    	            }
    	            return node.path;//返回经过的次数即可
    	        }

    好处:

    1.利用字符串的公共前缀来节约存储空间。

    2.最大限度地减少无谓的字符串比较,查询效率比较高。例如:若要查找的字符长度是5,而总共有单词的数目是26^5=11881376,利用trie树,利用5次比较可以从11881376个可能的关键字中检索出指定的关键字,而利用二叉查找树时间复杂度是O( log2n ),所以至少要进行log211881376=23.5次比较。可以看出来利用字典树进行查找速度是比较快的。

     

    应用:

    <1.字符串的快速检索

    <2.字符串排序

    <3.最长公共前缀:abdh和abdi的最长公共前缀是abd,遍历字典树到字母d时,此时这些单词的公共前缀是abd。

    <4.自动匹配前缀显示后缀

    我们使用辞典或者是搜索引擎的时候,输入appl,后面会自动显示一堆前缀是appl的东东吧。

    那么有可能是通过字典树实现的,前面也说了字典树可以找到公共前缀,我们只需要把剩余的后缀遍历显示出来即可。

     

    相关题目:

    一个字符串类型的数组arr1,另一个字符串类型的数组arr2。

    arr2中有哪些字符,是arr1中出现的?请打印。

    arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。

    arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。

     

    后缀树/后缀数组

    字典树:https://blog.csdn.net/hebtu666/article/details/83141560

    后缀树:后缀树,就是把一串字符的所有后缀保存并且压缩的字典树。

     

    相对于字典树来说,后缀树并不是针对大量字符串的,而是针对一个或几个字符串来解决问题。比如字符串的回文子串,两个字符串的最长公共子串等等。

    比如单词banana,它的所有后缀显示到下面的。0代表从第一个字符为起点,终点不用说都是字符串的末尾。

    以上面的后缀,我们建立一颗后缀树。如下图,为了方便看到后缀,我没有合并相同的前缀。

    把非公共部分压缩:

    后缀树的应用:

    (1)查找某个字符串s1是否在另外一个字符串s2中:如果s1在字符串s2中,那么s1必定是s2中某个后缀串的前缀。

    (2)指定字符串s1在字符串s2中重复的次数:比如说banana是s1,an是s2,那么计算an出现的次数实际上就是看an是几个后缀串的前缀。

    (3)两个字符串S1,S2的最长公共部分(广义后缀树)

    (4)最长回文串(广义后缀树)

     

    关于后缀树的实现和应用以后再写,这次主要写后缀数组。

    在字符串处理当中,后缀树和后缀数组都是非常有力的工具。其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多。可以说,在信息学竞赛中后缀数组比后缀树要更为实用。

     

    后缀数组:就是把某个字符串的所有后缀按照字典序排序后的数组。(数组中保存起始位置就好了,结束位置一定是最后)

    先说如何计算后缀数组:

    倍增的思想,我们先把每个长度为2的子串排序,再利用结果把每个长度为4的字串排序,再利用结果排序长度为8的子串。。。直到长度大于等于串长。

    设置sa[]数组来记录排名:sa[i]代表排第i名的是第几个串。

    结果用rank[]数组返回,rank[i]记录的是起始位置为第i个字符的后缀排名第几小。

    我们开始执行过程:

    比如字符串abracadabra

    长度为2的排名:a ab ab ac ad br br ca da ra ra,他们分别排第0,1,2,2,3,4,5,5,6,7,8,8名

    sa数组就是11(空串),10(a),0(ab),7,3,5,1,8,4,6,2,9(ra排名最后)

    这样,所有长度为2的子串的排名就出来了,我们如何利用排名把长度为4的排名搞出来呢?

    abracadabra中,ab,br,ra这些串排名知道了。我们把他们两两合并为长度为4的串,进行排名。

    比如abra和brac怎么比较呢?

    用原来排名的数对来表示

    abra=ab+ra=1+8

    brac=br+ac=4+2

    对于字符串的字典序,这个例子比1和4就比出来了。

    如果第一个数一样,也就是前两个字符一样,那再比后面就可以了。

    简单说就是先比前一半字符的排名,再比后一半的排名。

    具体实现,我们可以用系统sort,传一个比较器就好了。

     

    还有需要注意,长度不可能那么凑巧是2^n,所以 一般的,k=n时,rank[i]表示从位置i开始向后n个字符的排名第几小,而剩下不足看个字符,rank[i]代表从第i个字符到最后的串的排名第几小,也就是后缀。

    保证了每一个后缀都能正确表示并排序。比如k=4时,就表示出了长度为1,2,3的后缀:a,ra,bra.这就保证了k=8时,长度为5,6,7的后缀也能被表示出来:4+1,4+2,4+3

    还有,sa[0]永远是空串,空串的排名rank[sa[0]]永远是最大。

    int n;
    int k;
    int rank[MAX_N+1];//结果(排名)数组
    int tmp[MAX_N+1];//临时数组
    //定义比较器
    bool compare(int i,int j)
    {
        if(rank[i]!=rank[j])return rank[i]<rank[j];
        //长度为k的子串的比较
        int ri=i+k<=n ? rank[i+k] : -1;
        int rj=j+k<=n ? rank[j+k] : -1;
        return ri<rj;
    }
    
    void solve(string s,int *sa)
    {
        n=s.length;
        //长度为1时,按字符码即可,长度为2时就可以直接用
        for(int i=0;i<=n;i++)
        {
            sa[i]=i;
            rank[i]=i<n ? s[i] : -1;//注意空串为最大
        }
        //由k对2k排序,直到超范围
        for(k=1;k<=n;k*=2)
        {
            sort(sa,sa+n+1,compare);
            tmp[sa[0]=0;//空串
            for(int i=1;i<=n;i++)
            {
                tmp[sa[i]]=tmp[sa[i-1]]+(compare(sa[i-1],sa[i]) ? 1 : 0);//注意有相同的
            }
            for(int i=0;i<=n;i++)
            {
                rank[i]=tmp[i];
            }
        }
    }

    具体应用以后再写。。。。。

     

    AC自动机

    今天写一下基本的AC自动机的思想原理和实现。

    Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。

    KMP算法是单模式串的字符匹配算法,AC自动机是多模式串的字符匹配算法。

    首先我们回忆一下KMP算法:失配之后,子串通过next数组找到应该匹配的位置,也就是最长相等前后缀。

    AC自动机也是一样,只不过是匹配到当前失配之后,找到当前字符串的后缀,和所有字符串的前缀,找出最长相等前后缀。

    就这么简单。

    当然,字典树的知识是需要了解的。

    我就默认读者都会字典树了。

    我们操作的第一步就是把那些单词做一个字典树出来,这个好理解。

     

    在AC自动机中,我们也有类似next数组的东西就是fail指针,当发现失配的字符失配的时候,跳转到fail指针指向的位置,然后再次进行匹配操作

    当前节点t有fail指针,其fail指针所指向的节点和t所代表的字符是相同的。因为t匹配成功后,我们需要去匹配t->child,发现失配,那么就从t->fail这个节点开始再次去进行匹配。

    KMP里有详细讲解过程,我就不占篇幅叙述了。

    然后说一下fail指针如何建立:

    和next数组大同小异。如果你很熟悉next数组的建立,fail指针也是一样的。

    假设当前节点为father,其孩子节点记为child。求child的Fail指针时,首先我们要找到其father的Fail指针所指向的节点,假如是t的话,我们就要看t的孩子中有没有和child节点所表示的字母相同的节点,如果有的话,这个节点就是child的fail指针,如果发现没有,则需要找father->fail->fail这个节点,然后重复上面过程,如果一直找都找不到,则child的Fail指针就要指向root。

    KMP也是一样的的操作:p[next[i-1]]p[next[next[i-1]]]这样依次往前跳啊。

     

    如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。流程和NEXT数组类似。

     

    匹配的时候流程也是基本一样的,请参考KMP或者直接看代码:

    HDU 2222 Keywords Search    最基本的入门题了

    就是求目标串中出现了几个模式串。

    很基础了。使用一个int型的end数组记录,查询一次。

    #include <stdio.h>
    #include <algorithm>
    #include <iostream>
    #include <string.h>
    #include <queue>
    using namespace std;
    
    struct Trie
    {
        int next[500010][26],fail[500010],end[500010];
        int root,L;
        int newnode()
        {
            for(int i = 0;i < 26;i++)
                next[L][i] = -1;
            end[L++] = 0;
            return L-1;
        }
        void init()
        {
            L = 0;
            root = newnode();
        }
        void insert(char buf[])
        {
            int len = strlen(buf);
            int now = root;
            for(int i = 0;i < len;i++)
            {
                if(next[now][buf[i]-'a'] == -1)
                    next[now][buf[i]-'a'] = newnode();
                now = next[now][buf[i]-'a'];
            }
            end[now]++;
        }
        void build()//建树
        {
            queue<int>Q;
            fail[root] = root;
            for(int i = 0;i < 26;i++)
                if(next[root][i] == -1)
                    next[root][i] = root;
                else
                {
                    fail[next[root][i]] = root;
                    Q.push(next[root][i]);
                }
            while( !Q.empty() )//建fail
            {
                int now = Q.front();
                Q.pop();
                for(int i = 0;i < 26;i++)
                    if(next[now][i] == -1)
                        next[now][i] = next[fail[now]][i];
                    else
                    {
                        fail[next[now][i]]=next[fail[now]][i];
                        Q.push(next[now][i]);
                    }
            }
        }
        int query(char buf[])//匹配
        {
            int len = strlen(buf);
            int now = root;
            int res = 0;
            for(int i = 0;i < len;i++)
            {
                now = next[now][buf[i]-'a'];
                int temp = now;
                while( temp != root )
                {
                    res += end[temp];
                    end[temp] = 0;
                    temp = fail[temp];
                }
            }
            return res;
        }
        void debug()
        {
            for(int i = 0;i < L;i++)
            {
                printf("id = %3d,fail = %3d,end = %3d,chi = [",i,fail[i],end[i]);
                for(int j = 0;j < 26;j++)
                    printf("%2d",next[i][j]);
                printf("]\n");
            }
        }
    };
    char buf[1000010];
    Trie ac;
    int main()
    {
        int T;
        int n;
        scanf("%d",&T);
        while( T-- )
        {
            scanf("%d",&n);
            ac.init();
            for(int i = 0;i < n;i++)
            {
                scanf("%s",buf);
                ac.insert(buf);
            }
            ac.build();
            scanf("%s",buf);
            printf("%d\n",ac.query(buf));
        }
        return 0;
    }

     

    数组缺失

     

    二叉树遍历

    二叉树:二叉树是每个节点最多有两个子树的树结构。

     

    本文介绍二叉树的遍历相关知识。

    我们学过的基本遍历方法,无非那么几个:前序,中序,后序,还有按层遍历等等。

    设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

    首先我们定义一颗二叉树

    typedef char ElementType;
    typedef struct TNode *Position;
    typedef Position BinTree;
    struct TNode{
        ElementType Data;
        BinTree Left;
        BinTree Right;
    };
    

    前序

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树

    思路:

    就是利用函数,先打印本个节点,然后对左右子树重复此过程即可。

    void PreorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        printf(" %c", BT->Data);
        PreorderTraversal(BT->Left);
        PreorderTraversal(BT->Right);
    }

     

    中序

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树

    思路:

    还是利用函数,先对左边重复此过程,然后打印根,然后对右子树重复。

    void InorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        InorderTraversal(BT->Left);
        printf(" %c", BT->Data);
        InorderTraversal(BT->Right);
    }

    后序

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根

    思路:

    先分别对左右子树重复此过程,然后打印根

    void PostorderTraversal(BinTree BT)
    {
        if(BT==NULL)return ;
        PostorderTraversal(BT->Left);
        PostorderTraversal(BT->Right);
        printf(" %c", BT->Data);
    }

    进一步思考

    看似好像很容易地写出了三种遍历。。。。。

     

    但是你真的理解为什么这么写吗?

    比如前序遍历,我们真的是按照定义里所讲的,首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。这种过程来遍历了一遍二叉树吗?

    仔细想想,其实有一丝不对劲的。。。

    再看代码:

    void Traversal(BinTree BT)//遍历
    {
    //1111111111111
        Traversal(BT->Left);
    //22222222222222
        Traversal(BT->Right);
    //33333333333333
    }

    为了叙述清楚,我给三个位置编了号 1,2,3

    我们凭什么能前序遍历,或者中序遍历,后序遍历?

    我们看,前序中序后序遍历,实现的代码其实是类似的,都是上面这种格式,只是我们分别在位置1,2,3打印出了当前节点而已啊。我们凭什么认为,在1打印,就是前序,在2打印,就是中序,在3打印,就是后序呢?不管在位置1,2,3哪里操作,做什么操作,我们利用函数遍历树的顺序变过吗?当然没有啊。。。

    都是三次返回到当前节点的过程:先到本个节点,也就是位置1,然后调用了其他函数,最后调用完了,我们开到了位置2。然后又调用别的函数,调用完了,我们来到了位置3.。然后,最后操作完了,这个函数才结束。代码里的三个位置,每个节点都被访问了三次。

    而且不管位置1,2,3打印了没有,操作了没有,这个顺序是永远存在的,不会因为你在位置1打印了,顺序就改为前序,你在位置2打印了,顺序就成了中序。

     

    为了有更直观的印象,我们做个试验:在位置1,2,3全都放入打印操作;

    我们会发现,每个节点都被打印了三次。而把每个数第一次出现拿出来,就组成了前序遍历的序列;所有数字第二次出现拿出来,就组成了中序遍历的序列。。。。

     

    其实,遍历是利用了一种数据结构:栈

    而我们这种写法,只是通过函数,来让系统帮我们压了栈而已。为什么能实现遍历?为什么我们访问完了左子树,能返回到当前节点?这都是栈的功劳啊。我们把当前节点(对于函数就是当时的现场信息)存到了栈里,记录下来,后来才能把它拿了出来,能回到以前的节点。

     

    想到这里,可能就有更深刻的理解了。

    我们能否不用函数,不用系统帮我们压栈,而是自己做一个栈,来实现遍历呢?

    先序实现思路:拿到一个节点的指针,先判断是否为空,不为空就先访问(打印)该结点,然后直接进栈,接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲,然后访问其父亲的右子树,直到当前节点为空且栈为空时,结束。

    核心思路代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            //visit(p);进行操作
            st.push(p);//入栈
            p = p->lchild;左
        } 
        else//空
        {
            p = st.top();//取出
            st.pop();
            p = p->rchild;//右
        }
    }

    中序实现思路:和前序遍历一样,只不过在访问节点的时候顺序不一样,访问节点的时机是从栈中弹出元素时访问,如果从栈中弹出元素,就意味着当前节点父亲的左子树已经遍历完成,这时候访问父亲,就是中序遍历.

    (对应递归是第二次遇到)

    核心代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            st.push(p);//压入
            p = p->lchild;
        }
        else//空
        {
            p = st.top();//取出
            //visit(p);操作
            st.pop();
            p = p->rchild;
        }
    }

    后序遍历是最难的。因为要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难点。

    因为我们原来说了,后序是第三次遇到才进行操作的,所以我们很容易有这种和递归函数类似的思路:对于任一结点,将其入栈,然后沿其左子树一直往下走,一直走到没有左孩子的结点,此时该结点在栈顶,但是不能出栈访问, 因此右孩子还没访问。所以接下来按照相同的规则对其右子树进行相同的处理。访问完右孩子,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

    第二种思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被访问过了,就可以直接访问该结点。如果有孩子未访问,将P的右孩子和左孩子依次入栈。

    网上的思路大多是第一种,所以我在这里给出第二种的大概实现吧

    首先初始化cur,pre两个指针,代表访问的当前节点和之前访问的节点。把根放入,开始执行。

    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))
        {
            //visit(cur);  如果当前结点没有孩子结点或者孩子节点都已被访问过 
            s.pop();//弹出
            pre=cur; //记录
        }
        else//分别放入右左孩子
        {
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)    
                s.push(cur->lchild);
        }
    }

    这两种方法,都是利用栈结构来实现的遍历,需要一定的栈空间,而其实存在一种时间O(N),空间O(1)的遍历方式,下次写了我再放链接。

     

    斗个小机灵:后序是LRD,我们其实已经知道先序是DLR,那其实我们可以用先序来实现后序啊,我们只要先序的时候把左右子树换一下:DRL(这一步很好做到),然后倒过来不就是DRL了嘛。。。。。就把先序代码改的左右反过来,然后放栈里倒过来就好了,不需要上面介绍的那些复杂的方法。。。。

     

    二叉树序列化/反序列化

    二叉树被记录成文件的过程,为二叉树的序列化

    通过文件重新建立原来的二叉树的过程,为二叉树的反序列化

    设计方案并实现。

    (已知结点类型为32位整型)

     

    思路:先序遍历实现。

    因为要写入文件,我们要把二叉树序列化为一个字符串。

    首先,我们要规定,一个结点结束后的标志:“!”

    然后就可以通过先序遍历生成先序序列了。

     

    但是,众所周知,只靠先序序列是无法确定一个唯一的二叉树的,原因分析如下:

    比如序列1!2!3!

    我们知道1是根,但是对于2,可以作为左孩子,也可以作为右孩子:

    对于3,我们仍然无法确定,应该作为左孩子还是右孩子,情况显得更加复杂:

    原因:我们对于当前结点,插入新结点是无法判断插入位置,是应该作为左孩子,还是作为右孩子。

    因为我们的NULL并未表示出来。

    如果我们把NULL也用一个符号表示出来:

    比如

    1!2!#!#!3!#!#!

    我们再按照先序遍历的顺序重建:

    对于1,插入2时,就确定要作为左孩子,因为左孩子不为空。

    然后接下来两个#,我们就知道了2的左右孩子为空,然后重建1的右子树即可。

     

    我们定义结点:

    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    序列化:

    	public static String serialByPre(Node head) {
    		if (head == null) {
    			return "#!";
    		}
    		String res = head.value + "!";
    		res += serialByPre(head.left);
    		res += serialByPre(head.right);
    		return res;
    	}

     

    	public static Node reconByPreString(String preStr) {
            //先把字符串转化为结点序列
    		String[] values = preStr.split("!");
    		Queue<String> queue = new LinkedList<String>();
    		for (int i = 0; i != values.length; i++) {
    			queue.offer(values[i]);
    		}
    		return reconPreOrder(queue);
    	}
    
    	public static Node reconPreOrder(Queue<String> queue) {
    		String value = queue.poll();
    		if (value.equals("#")) {
    			return null;//遇空
    		}
    		Node head = new Node(Integer.valueOf(value));
    		head.left = reconPreOrder(queue);
    		head.right = reconPreOrder(queue);
    		return head;
    	}

    这样并未改变先序遍历的时空复杂度,解决了先序序列确定唯一一颗树的问题,实现了二叉树序列化和反序列化。

     

    先序中序后序两两结合重建二叉树

    遍历是对树的一种最基本的运算,所谓遍历二叉树,就是按一定的规则和顺序走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。

    设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

    先序遍历

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树,C语言代码如下:

    1

    2

    3

    4

    5

    6

    7

    void XXBL(tree *root){

        //DoSomethingwithroot

        if(root->lchild!=NULL)

            XXBL(root->lchild);

        if(root->rchild!=NULL)

            XXBL(root->rchild);

    }

    中序遍历

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树,C语言代码如下

    1

    2

    3

    4

    5

    6

    7

    8

    void ZXBL(tree *root)

    {

        if(root->lchild!=NULL)

            ZXBL(root->lchild);

            //Do something with root

        if(root->rchild!=NULL)

            ZXBL(root->rchild);

    }

    后序遍历

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根,C语言代码如下

    1

    2

    3

    4

    5

    6

    7

    void HXBL(tree *root){

        if(root->lchild!=NULL)

            HXBL(root->lchild);

        if(root->rchild!=NULL)

            HXBL(root->rchild);

            //Do something with root

    }

    层次遍历

    即按照层次访问,通常用队列来做。访问根,访问子女,再访问子女的子女(越往后的层次越低)(两个子女的级别相同)

     

    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

     

    我们首先找到根结点:一定是先序遍历序列的第一个元素:1

    然后,在中序序列寻找根,把中序序列分为两个序列左子树4,7,2和右子树5,3,8,6

    把先序序列也分为两个:                                           左子树2,4,7和右子树3,5,6,8

    对左右重复同样的过程:

    先看左子树:先序序列4,7,2,说明4一定是左子树的根

    把2,4,7分为2和7两个序列,再重复过程,左边确定完毕。

    右子树同样:中序序列为5,3,8,6,先序序列为:3,5,6,8

    取先序头,3.一定是根

    把中序序列分为     5和8,6两个序列

    对应的先序序列为 5和6,8两个序列

     

    然后确定了5是3的左孩子

    对于先序序列6,8和中序序列8,6

    还是先取先序的头,6

     

    现在只有8,中序序列8在左边,是左孩子。

    结束。

    我们总结一下这种方法的过程:

    1、根据先序序列确定当前树的根(第一个元素)。

    2、在中序序列中找到根,并以根为分界分为两个序列。

    3、这样,确定了左子树元素个数,把先序序列也分为两个。

    对左右子树(对应的序列)重复相同的过程。

     

    我们把思路用代码实现:

    # -*- coding:utf-8 -*-
    # class TreeNode:
    #     def __init__(self, x):
    #         self.val = x
    #         self.left = None
    #         self.right = None
    class Solution:
        # 返回构造的TreeNode根节点
        def reConstructBinaryTree(self, pre, tin):
            # write code here/
            #pre-先序数组   tin->中序数组
            if len(pre) == 0:
                return None
            root = TreeNode(pre[0])//第一个元素为根
            pos = tin.index(pre[0])//划分左右子树
            root.left = self.reConstructBinaryTree( pre[1:1+pos], tin[:pos])
            root.right = self.reConstructBinaryTree( pre[pos+1:], tin[pos+1:])
            return root
    

    输入某二叉树的后序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

     

    思路是类似的,只是我们确定根的时候,取后序序列的最后一个元素即可。

     

    输入某二叉树的后序遍历和先序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字

     

    我们直白的表述一下,前序是中左右,后序是左右中。

    所以,我们凭先序和后序序列其实是无法判断根的孩子到底是左孩子还是右孩子。

    比如先序序列1,5,后序序列是5,1

    我们只知道1是这棵树的根,但是我们不知道5是1的左孩子还是右孩子。

    我们的中序序列是左中右,才可以明确的划分出左右子树,而先序后序不可以。

     

    综上,只有,只含叶子结点或者同时有左右孩子的结点的树,才可以被先序序列后序序列确定唯一一棵树。

    最后不断划分先序和后序序列完成重建。

     

    先序中序数组推后序数组

    二叉树遍历

    所谓遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问 题。 遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算之基础。

     

    从二叉树的递归定义可知,一棵非空的二叉树由根结点及左、右子树这三个基本部分组成。因此,在任一给定结点上,可以按某种次序执行三个操作:

    ⑴访问结点本身(N),

    ⑵遍历该结点的左子树(L),

    ⑶遍历该结点的右子树(R)。

    以上三种操作有六种执行次序:

    NLR、LNR、LRN、NRL、RNL、RLN。

    注意:

    前三种次序与后三种次序对称,故只讨论先左后右的前三种次序。

    遍历命名

    根据访问结点操作发生位置命名:

    ① NLR:前序遍历(Preorder Traversal 亦称(先序遍历))

    ——访问根结点的操作发生在遍历其左右子树之前。

    ② LNR:中序遍历(Inorder Traversal)

    ——访问根结点的操作发生在遍历其左右子树之中(间)。

    ③ LRN:后序遍历(Postorder Traversal)

    ——访问根结点的操作发生在遍历其左右子树之后。

    注意:

    由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

     

    给出某棵树的先序遍历结果和中序遍历结果(无重复值),求后序遍历结果。

    比如

    先序序列为:1,2,4,5,3,6,7,8,9

    中序序列为:4,2,5,1,6,3,7,9,8

    方法1:我们可以重建整棵树:

    https://blog.csdn.net/hebtu666/article/details/84322113

    建议好好看这个网址,对理解这个方法有帮助。

     

    如图

    然后后序遍历得出后序序列。

     

    方法2:我们可以不用重建,直接得出:

    过程:

    1)根据当前先序数组,设置后序数组最右边的值

    2)划分出左子树的先序、中序数组和右子树的先序、中序数组

    3)对右子树重复同样的过程

    4)对左子树重复同样的过程

     

    原因:我们的后序遍历是左右中的,也就是先左子树,再右子树,再根

    举个例子:

    比如这是待填充序列:

    我们确定了根,并且根据根和中序序列划分出了左右子树,黄色部分为左子树:

    先处理右子树(其实左右中反过来就是中右左,顺着填就好了):

    我们又确定了右子树的右子树为黑色区域,然后接着填右子树的右子树的根(N)即可。

     

     

    举例说明:

    a[]先序序列为:1,2,4,5,3,6,7,8,9

    b[]中序序列为:4,2,5,1,6,3,7,9,8

    c[]后序序列为:0,0,0,0,0,0,0,0,0(0代表未确定)

    我们根据先序序列,知道根一定是1,所以后序序列:0,0,0,0,0,0,0,0,1

    从b[]中找到1,并划分数组:

              左子树的先序:2,4,5,

              中序:4,2,5

              右子树的先序:3,6,7,8,9,

              中序:6,3,7,9,8

     

    我们继续对右子树重复相同的过程:

    (图示为当前操作的树,我们是不知道这棵树的样子的,我是为了方便叙述,图片表达一下当前处理的位置)

    当前树的根一定为先序序列的第一个元素,3,所以我们知道后序序列:0,0,0,0,0,0,0,3,1

    我们继续对左右子树进行划分,中序序列为6,3,7,9,8,我们在序列中找到2,并划分为左右子树:

    左子树:

    先序序列:6

    中序序列:6

    右子树:

    先序序列:7,8,9

    中序序列:7,9,8

    我们继续对右子树重复相同的过程,也就是如图所示的这棵树:

    现在我们的后序序列为0,0,0,0,0,0,0,3,1

    这时我们继续取当前的根(先序第一个元素)放在下一个后序位置:0,0,0,0,0,0,7,3,1

    划分左右子树:

    左子树:空,也就是它

    右子树:先序8,9,中序9,8,也就是这个树

    我们继续处理右子树:先序序列为8,9,所以根为8,我们继续填后序数组0,0,0,0,0,8,7,3,1

    然后划分左右子树:

    左子树:先序:9,中序:9

    右子树:空

    对于左子树,一样,我们取头填后序数组0,0,0,0,9,8,7,3,1,然后发现左右子树都为空.

    我们就把这个小框框处理完了

    然后这棵树的右子树就处理完了,处理左子树,发现为空。这棵树也处理完了。

    这一堆就完了。我们处理以3为根的二叉树的左子树。继续填后序数组:

    0,0,0,6,9,8,7,3,1

    整棵树的右子树处理完了,左子树同样重复这个过程。

    最后4,5,2,6,9,8,7,3,1

     

    好累啊。。。。。。挺简单个事写了这么多。

    回忆一下过程:

    1)根据当前先序数组,设置后序数组最右边的值

    2)划分出左子树的先序、中序数组和右子树的先序、中序数组

    3)对右子树重复同样的过程

    4)对左子树重复同样的过程

    就这么简单

     

    先填右子树是为了数组连续填充,容易理解,先处理左子树也可以。

    最后放上代码吧

    a=[1,2,4,5,3,6,7,8,9]
    b=[4,2,5,1,6,3,7,9,8]
    l=[0,0,0,0,0,0,0,0,0]
    
    def f(pre,tin,x,y):
        #x,y为树在后序数组中对应的范围
        if pre==[]:return
        l[y]=pre[0]#根
        pos=tin.index(pre[0])#左子树元素个数
        f(pre[pos+1:],tin[pos+1:],x+pos,y-1)#处理右子树
        f(pre[1:pos+1],tin[:pos],x,x+pos-1)#处理左子树
        
    f(a,b,0,len(l)-1)
    print(l)
    

    根据数组建立平衡二叉搜索树

    它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉(搜索)树。

     

    二分:用有序数组中中间的数生成搜索二叉树的头节点,然后对数组的左右部分分别生成左右子树即可(重复过程)。

    生成的二叉树中序遍历一定还是这个序列。

     

    非常简单,不过多叙述:

    public class SortedArrayToBalancedBST {
    
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    
    	public static Node generateTree(int[] sortArr) {
    		if (sortArr == null) {
    			return null;
    		}
    		return generate(sortArr, 0, sortArr.length - 1);
    	}
    
    	public static Node generate(int[] sortArr, int start, int end) {
    		if (start > end) {
    			return null;
    		}
    		int mid = (start + end) / 2;
    		Node head = new Node(sortArr[mid]);
    		head.left = generate(sortArr, start, mid - 1);
    		head.right = generate(sortArr, mid + 1, end);
    		return head;
    	}
    
    	// for test -- print tree
    	public static void printTree(Node head) {
    		System.out.println("Binary Tree:");
    		printInOrder(head, 0, "H", 17);
    		System.out.println();
    	}
    
    	public static void printInOrder(Node head, int height, String to, int len) {
    		if (head == null) {
    			return;
    		}
    		printInOrder(head.right, height + 1, "v", len);
    		String val = to + head.value + to;
    		int lenM = val.length();
    		int lenL = (len - lenM) / 2;
    		int lenR = len - lenM - lenL;
    		val = getSpace(lenL) + val + getSpace(lenR);
    		System.out.println(getSpace(height * len) + val);
    		printInOrder(head.left, height + 1, "^", len);
    	}
    
    	public static String getSpace(int num) {
    		String space = " ";
    		StringBuffer buf = new StringBuffer("");
    		for (int i = 0; i < num; i++) {
    			buf.append(space);
    		}
    		return buf.toString();
    	}
    
    	public static void main(String[] args) {
    		int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    		printTree(generateTree(arr));
    
    	}
    
    }
    

    java整体打印二叉树

    一个调的很好的打印二叉树的代码。

    用空格和^v来表示节点之间的关系。

    效果是这样:

    Binary Tree:
                                             v7v       
                            v6v       
                                             ^5^       
           H4H       
                                             v3v       
                            ^2^       
                                             ^1^  

     

    对于每个节点,先打印右子树,然后打印本身,然后打印左子树。

     

    public class fan {
    	public static class Node {
    		public int value;
    		Node left;
    		Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	
    	public static void printTree(Node head) {
    		System.out.println("Binary Tree:");
    		printInOrder(head, 0, "H", 17);
    		System.out.println();
    	}
    	
    	public static void printInOrder(Node head, int height, String to, int len) {
    		if (head == null) {
    			return;
    		}
    		printInOrder(head.right, height + 1, "v", len);
    		String val = to + head.value + to;
    		int lenM = val.length();
    		int lenL = (len - lenM) / 2;
    		int lenR = len - lenM - lenL;
    		val = getSpace(lenL) + val + getSpace(lenR);
    		System.out.println(getSpace(height * len) + val);
    		printInOrder(head.left, height + 1, "^", len);
    	}
    
    	public static String getSpace(int num) {
    		String space = " ";
    		StringBuffer buf = new StringBuffer("");
    		for (int i = 0; i < num; i++) {
    			buf.append(space);
    		}
    		return buf.toString();
    	}
    
    	public static void main(String[] args) {
    		Node head = new Node(4);
    		head.left = new Node(2);
    		head.right = new Node(6);
    		head.left.left = new Node(1);
    		head.left.right = new Node(3);
    		head.right.left = new Node(5);
    		head.right.right = new Node(7);
    		printTree(head);
    
    	}
    
    }
    

    判断平衡二叉树

    平衡二叉树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1。并且左右两个子树都是一棵平衡二叉树

    (不是我们平时意义上的必须为搜索树)

    判断一棵树是否为平衡二叉树:

     

    可以暴力判断:每一颗树是否为平衡二叉树。

     

    分析:

    如果左右子树都已知是平衡二叉树,而左子树和右子树高度差绝对值不超过1,本树就是平衡的。

     

    为此我们需要的信息:左右子树是否为平衡二叉树。左右子树的高度。

     

    我们需要给父返回的信息就是:本棵树是否是平衡的、本棵树的高度。

     

    定义结点和返回值:

    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	public static class ReturnType {
    		public int level;   //深度
    		public boolean isB;//本树是否平衡
    		
    		public ReturnType(int l, boolean is) {
    			level = l;
    			isB = is;
    		}
    	}

    我们把代码写出来:

    	// process(head, 1)
    	
    	public static ReturnType process(Node head, int level) {
    		if (head == null) {
    			return new ReturnType(level, true);
    		}
    		//取信息
    		ReturnType leftSubTreeInfo = process(head.left, level + 1);
    		if(!leftSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //左子树不是->返回
    		}
    		ReturnType rightSubTreeInfo = process(head.right, level + 1);
    		if(!rightSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //右子树不是->返回
    		}
    		if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {
    			return new ReturnType(level, false);     //左右高度差大于1->返回
    		}
    		
    		return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);
    		//返回高度和true(当前树是平衡的)
    	}

    我们不需要每次都返回高度,用一个全局变量记录即可。

    对于其它二叉树问题,可能不止一个变量信息,所以,全局记录最好都养成定义数组的习惯。

    下面贴出完整代码:

    import java.util.LinkedList;
    import java.util.Queue;
    
    public class Demo {
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	public static boolean isBalance(Node head) {
    		boolean[] res = new boolean[1];
    		res[0] = true;
    		getHeight(head, 1, res);
    		return res[0];
    	}
    	
    	public static class ReturnType {
    		public int level;   //深度
    		public boolean isB;//本树是否平衡
    		
    		public ReturnType(int l, boolean is) {
    			level = l;
    			isB = is;
    		}
    	}
    	
    	// process(head, 1)
    	
    	public static ReturnType process(Node head, int level) {
    		if (head == null) {
    			return new ReturnType(level, true);
    		}
    		//取信息
    		ReturnType leftSubTreeInfo = process(head.left, level + 1);
    		if(!leftSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //左子树不是->返回
    		}
    		ReturnType rightSubTreeInfo = process(head.right, level + 1);
    		if(!rightSubTreeInfo.isB) {
    			return new ReturnType(level, false);     //右子树不是->返回
    		}
    		if (Math.abs(rightSubTreeInfo.level - leftSubTreeInfo.level) > 1) {
    			return new ReturnType(level, false);     //左右高度差大于1->返回
    		}
    		
    		return new ReturnType(Math.max(leftSubTreeInfo.level, rightSubTreeInfo.level), true);
    		//返回高度和true(当前树是平衡的
    	}
    
    	public static int getHeight(Node head, int level, boolean[] res) {
    		if (head == null) {
    			return level;//返回高度
    		}
    		//取信息
    		//相同逻辑
    		int lH = getHeight(head.left, level + 1, res);
    		if (!res[0]) {
    			return level;
    		}
    		int rH = getHeight(head.right, level + 1, res);
    		if (!res[0]) {
    			return level;
    		}
    		if (Math.abs(lH - rH) > 1) {
    			res[0] = false;
    		}
    		return Math.max(lH, rH);//返回高度
    	}
    
    	public static void main(String[] args) {
    		Node head = new Node(1);
    		head.left = new Node(2);
    		head.right = new Node(3);
    		head.left.left = new Node(4);
    		head.left.right = new Node(5);
    		head.right.left = new Node(6);
    		head.right.right = new Node(7);
    
    		System.out.println(isBalance(head));
    
    	}
    
    }

    判断完全二叉树

    完全二叉树的定义: 一棵二叉树,除了最后一层之外都是完全填充的,并且最后一层的叶子结点都在左边。

    https://baike.baidu.com/item/%E5%AE%8C%E5%85%A8%E4%BA%8C%E5%8F%89%E6%A0%91/7773232?fr=aladdin

    百度定义

     

    思路:层序遍历二叉树

    如果一个结点,左右孩子都不为空,则pop该节点,将其左右孩子入队列

    如果一个结点,左孩子为空,右孩子不为空,则该树一定不是完全二叉树

    如果一个结点,左孩子不为空,右孩子为空;或者左右孩子都为空:::::则该节点之后的队列中的结点都为叶子节点;该树才是完全二叉树,否则返回false。

    非完全二叉树的例子(对应方法的正确性和必要性):

    下面写代码:

    定义结点:

        public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    方法:

    	public static boolean isCBT(Node head) {
    		if (head == null) {
    			return true;
    		}
    		Queue<Node> queue = new LinkedList<Node>();
    		boolean leaf = false;
    		Node l = null;
    		Node r = null;
    		queue.offer(head);
    		while (!queue.isEmpty()) {
    			head = queue.poll();
    			l = head.left;
    			r = head.right;
    			if ((leaf && (l != null || r != null)) || (l == null && r != null)) {
    				return false;//当前结点不是叶子结点且之前结点有叶子结点 || 当前结点有右孩子无左孩子
    			}
    			if (l != null) {
    				queue.offer(l);
    			}
    			if (r != null) {
    				queue.offer(r);
    			} else {
    				leaf = true;//无孩子即为叶子结点
    			}
    		}
    		return true;
    	}

    判断二叉搜索树

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树

     

    判断某棵树是否为二叉搜索树

     

    单纯判断每个结点比左孩子大比右孩子小是不对的。如图:

    15推翻了这种方法。

     

    思路:

    1)可以根据定义判断,递归进行,如果左右子树都为搜索二叉树,且左子树最大值小于根,右子树最小值大于根。成立。

    2)根据定义,中序遍历为递增序列,我们中序遍历后判断是否递增即可。

    3)我们可以在中序遍历过程中判断之前节点和当前结点的关系,不符合直接返回false即可。

    4)进一步通过morris遍历优化

    morris遍历:https://blog.csdn.net/hebtu666/article/details/83093983

     

    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    	public static boolean isBST(Node head) {
    		if (head == null) {
    			return true;
    		}
    		boolean res = true;
    		Node pre = null;
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}
    			}
    			if (pre != null && pre.value > cur1.value) {
    				res = false;
    			}
    			pre = cur1;
    			cur1 = cur1.right;
    		}
    		return res;
    	}

    二叉搜索树实现

    本文给出二叉搜索树介绍和实现

     

    首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

     

    那这个结构有什么有用呢?

    首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

    基本操作:

    1、插入某个数值

    2、查询是否包含某个数值

    3、删除某个数值

     

    根据实现不同,还可以实现其他很多种操作。

     

    实现思路思路:

    前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

    而删除稍微复杂一些,有下面这几种情况:

    1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

    2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

    3、以上都不满足,就把左儿子子孙中最大节点提上来。

     

    当然,反过来也是成立的,比如右儿子子孙中最小的节点。

     

    下面来叙述为什么可以这么做。

    下图中A为待删除节点。

    第一种情况:

     

    1、去掉A,把c提上来,c也是小于x的没问题。

    2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

     

    第二种情况

     

    3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

    4、同理

     

    第三种情况

     

    5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

    因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

    而黑点小于A,也就小于c,所以可以让c当右孩子

    大概证明就这样。。

    下面我们用代码实现并通过注释理解

    上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

    定义

    struct node
    {
        int val;//数据
        node *lch,*rch;//左右孩子
    };

    插入

     node *insert(node *p,int x)
     {
         if(p==NULL)//直到空就创建节点
         {
             node *q=new node;
             q->val=x;
             q->lch=q->rch=NULL;
             return p;
         }
         if(x<p->val)p->lch=insert(p->lch,x);
         else p->lch=insert(p->rch,x);
         return p;//依次返回自己,让上一个函数执行。
     }

    查找

     bool find(node *p,int x)
     {
         if(p==NULL)return false;
         else if(x==p->val)return true;
         else if(x<p->val)return find(p->lch,x);
         else return find(p->rch,x);
     }

    删除

     node *remove(node *p,int x)
     {
          if(p==NULL)return NULL;
          else if(x<p->val)p->lch=remove(p->lch,x);
          else if(x>p->val)p->lch=remove(p->rch,x);
          //以下为找到了之后
          else if(p->lch==NULL)//情况1
          {
              node *q=p->rch;
              delete p;
              return q;
          }
          else if(p->lch->rch)//情况2
          {
              node *q=p->lch;
              q->rch=p->rch;
              delete p;
              return q;
          }
          else
          {
              node *q;
              for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个
              node *r=q->rch;//最大节点
              q->rch=r->lch;//最大节点左孩子提到最大节点位置
              r->lch=p->lch;//调整黑点左孩子为B
              r->rch=p->rch;//调整黑点右孩子为c
              delete p;//删除
              return r;//返回给父
          }
          return p;
     }

    堆的简单实现

    关于堆不做过多介绍

    堆就是儿子的值一定不小于父亲的值并且树的节点都是按照从上到下,从左到右紧凑排列的树。

    (本文为二叉堆)

    具体实现并不需要指针二叉树,用数组储存并且利用公式找到父子即可。

    父:(i-1)/2

    子:i*2+1,i*2+2

    插入:首先把新数字放到堆的末尾,也就是右下角,然后查看父的数值,需要交换就交换,重复上述操作直到不需交换

    删除:把堆的第一个节点赋值为最后一个节点的值,然后删除最后一个节点,不断向下交换。

    (两个儿子:严格来说要选择数值较小的那一个)

    时间复杂度:和深度成正比,所以n个节点是O(logN)

    int heap[MAX_N],sz=0;
    //定义数组和记录个数的变量

    插入代码:

    void push(int x)
    {//节点编号
        int i=sz++;
        while(i>0)
        {
            int p=(i-1)/2;//父
            if(heap[p]<=x)break;//直到大小顺序正确跳出循环
            heap[i]=heap[p];//把父节点放下来
            i=p;
        }
        heap[i]=x;//最后把自己放上去
        
    }

    弹出:

    int pop()
    {
        int ret=heap[0];//保存好值,最后返回
        int x=heap[--sz];
        while(i*2+1<sz)
        {
            int a=i*2+1;//左孩子
            int b=i*2+2;//右孩子
            if(b<sz && heap[b]<heap[a])a=b;//找最小
            if(heap[a]>=x)break;//直到不需要交换就退出
            heap[i]=heap[a];//把儿子放上来
            i=a;
        }
        head[i]=x;//下沉到正确位置
        return ret;//返回
    }

    堆应用例题三连

    一个数据流中,随时可以取得中位数。


    题目描述:有一个源源不断地吐出整数的数据流,假设你有足够的空间来保存吐出的数。请设计一个名叫MedianHolder的结构,MedianHolder可以随时取得之前吐出所有树的中位数。

    要求:

    1.如果MedianHolder已经保存了吐出的N个数,那么任意时刻将一个新的数加入到MedianHolder的过程中,时间复杂度O(logN)。

    2.取得已经吐出的N个数整体的中位数的过程,时间复杂度O(1).

     

    看这要求就应该感觉到和堆相关吧?

    但是进一步没那么好想。

    设计的MedianHolder中有两个堆,一个是大根堆,一个是小根堆。大根堆中含有接收的所有数中较小的一半,并且按大根堆的方式组织起来,那么这个堆的堆顶就是较小一半的数中最大的那个。小根堆中含有接收的所有数中较大的一半,并且按小根堆的方式组织起来,那么这个堆的堆顶就是较大一半的数中最小的那个。

    例如,如果已经吐出的数为6,1,3,0,9,8,7,2.

    较小的一半为:0,1,2,3,那么3就是这一半的数组成的大根堆的堆顶

    较大的一半为:6,7,8,9,那么6就是这一半的数组成的小根堆的堆顶

    因为此时数的总个数为偶数,所以中位数就是两个堆顶相加,再除以2.

    如果此时新加入一个数10,那么这个数应该放进较大的一半里,所以此时较大的一半数为6,7,8,9,10,此时6依然是这一半的数组成的小根堆的堆顶,因为此时数的总个数为奇数,所以中位数应该是正好处在中间位置的数,而此时大根堆有4个数,小根堆有5个数,那么小根堆的堆顶6就是此时的中位数。

    如果此时又新加入一个数11,那么这个数也应该放进较大的一半里,此时较大一半的数为:6,7,8,9,10,11.这个小根堆大小为6,而大根堆的大小为4,所以要进行如下调整:

    1.如果大根堆的size比小根堆的size大2,那么从大根堆里将堆顶元素弹出,并放入小根堆里

    2,如果小根堆的size比大根堆的size大2,那么从小根堆里将堆顶弹出,并放入大根堆里。

    经过这样的调整之后,大根堆和小根堆的size相同。

    总结如下:

    大根堆每时每刻都是较小的一半的数,堆顶为这一堆数的最大值
    小根堆每时每刻都是较大的一半的数,堆顶为这一堆数的最小值
    新加入的数根据与两个堆堆顶的大小关系,选择放进大根堆或者小根堆里(或者放进任意一个堆里)
    当任何一个堆的size比另一个size大2时,进行如上调整的过程。


    这样随时都可以知道已经吐出的所有数处于中间位置的两个数是什么,取得中位数的操作时间复杂度为O(1),同时根据堆的性质,向堆中加一个新的数,并且调整堆的代价为O(logN)。
     

    import java.util.Arrays;
    import java.util.Comparator;
    import java.util.PriorityQueue;
     
    /**
     * 随时找到数据流的中位数
     * 思路:
     * 利用一个大根堆和一个小根堆去保存数据,保证前一半的数放在大根堆,后一半的数放在小根堆
     * 在添加数据的时候,不断地调整两个堆的大小,使得两个堆保持平衡
     * 要取得的中位数就是两个堆堆顶的元素
     */
    public class MedianQuick {
        public static class MedianHolder {
            private PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(new MaxHeapComparator());
            private PriorityQueue<Integer> minHeap = new PriorityQueue<Integer>(new MinHeapComparator());
     
            /**
             * 调整堆的大小
             * 当两个堆的大小差值变大时,从数据多的堆中弹出一个数据进入另一个堆中
             */
            private void modifyTwoHeapsSize() {
                if (this.maxHeap.size() == this.minHeap.size() + 2) {
                    this.minHeap.add(this.maxHeap.poll());
                }
                if (this.minHeap.size() == this.maxHeap.size() + 2) {
                    this.maxHeap.add(this.minHeap.poll());
                }
            }
     
            /**
             * 添加数据的过程
             *
             * @param num
             */
            public void addNumber(int num) {
                if (this.maxHeap.isEmpty()) {
                    this.maxHeap.add(num);
                    return;
                }
                if (this.maxHeap.peek() >= num) {
                    this.maxHeap.add(num);
                } else {
                    if (this.minHeap.isEmpty()) {
                        this.minHeap.add(num);
                        return;
                    }
                    if (this.minHeap.peek() > num) {
                        this.maxHeap.add(num);
                    } else {
                        this.minHeap.add(num);
                    }
                }
                modifyTwoHeapsSize();
            }
     
            /**
             * 获取中位数
             *
             * @return
             */
            public Integer getMedian() {
                int maxHeapSize = this.maxHeap.size();
                int minHeapSize = this.minHeap.size();
                if (maxHeapSize + minHeapSize == 0) {
                    return null;
                }
                Integer maxHeapHead = this.maxHeap.peek();
                Integer minHeapHead = this.minHeap.peek();
                if (((maxHeapSize + minHeapSize) & 1) == 0) {
                    return (maxHeapHead + minHeapHead) / 2;
                }
                return maxHeapSize > minHeapSize ? maxHeapHead : minHeapHead;
            }
        }
     
        /**
         * 大根堆比较器
         */
        public static class MaxHeapComparator implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o2 > o1) {
                    return 1;
                } else {
                    return -1;
                }
            }
        }
     
        /**
         * 小根堆比较器
         */
        public static class MinHeapComparator implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o2 < o1) {
                    return 1;
                } else {
                    return -1;
                }
            }
        }
     
        // for test
        public static int[] getRandomArray(int maxLen, int maxValue) {
            int[] res = new int[(int) (Math.random() * maxLen) + 1];
            for (int i = 0; i != res.length; i++) {
                res[i] = (int) (Math.random() * maxValue);
            }
            return res;
        }
     
        // for test, this method is ineffective but absolutely right
        public static int getMedianOfArray(int[] arr) {
            int[] newArr = Arrays.copyOf(arr, arr.length);
            Arrays.sort(newArr);
            int mid = (newArr.length - 1) / 2;
            if ((newArr.length & 1) == 0) {
                return (newArr[mid] + newArr[mid + 1]) / 2;
            } else {
                return newArr[mid];
            }
        }
     
        public static void printArray(int[] arr) {
            for (int i = 0; i != arr.length; i++) {
                System.out.print(arr[i] + " ");
            }
            System.out.println();
        }
     
        public static void main(String[] args) {
            boolean err = false;
            int testTimes = 200000;
            for (int i = 0; i != testTimes; i++) {
                int len = 30;
                int maxValue = 1000;
                int[] arr = getRandomArray(len, maxValue);
                MedianHolder medianHold = new MedianHolder();
                for (int j = 0; j != arr.length; j++) {
                    medianHold.addNumber(arr[j]);
                }
                if (medianHold.getMedian() != getMedianOfArray(arr)) {
                    err = true;
                    printArray(arr);
                    break;
                }
            }
            System.out.println(err ? "Oops..what a fuck!" : "today is a beautiful day^_^");
     
        }
    }
    

    金条

     

    一块金条切成两半,是需要花费和长度数值一样的铜板的。比如长度为20的金条,不管切成长度多大的两半,都要花费20个铜板。一群人想整分整块金条,怎么分最省铜板?
    例如,给定数组{10,20,30},代表一共三个人,整块金条长度为10+20+30=60,金条要分成10,20,30三个部分。如果,先把长度60的金条分成10和50,花费60,再把长度为50的金条分成20和30,花费50,一共花费110个铜板。

    但是如果,先把长度60的金条分成30和30,花费60,再把长度30金条分成10和30,花费30,一共花费90个铜板。

    输入一个数组,返回分割的最小代价。

    首先我们要明白一点:不管合并策略是什么我们一共会合并n-1次,这个次数是不会变的。

    我们要做的就是每一次都做最优选择。

    合为最优?

    最小的两个数合并就是最优。

    所以

    1)首先构造小根堆

    2)每次取最小的两个数(小根堆),使其代价最小。并将其和加入到小根堆中

    3)重复(2)过程,直到最后堆中只剩下一个节点。

     

    花费为每次花费的累加。

    代码略。

     

    项目最大收益(贪心问题)


    输入:参数1,正数数组costs,参数2,正数数组profits,参数3,正数k,参数4,正数m

    costs[i]表示i号项目的花费profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润),k表示你不能并行,只能串行的最多做k个项目,m表示你初始的资金。

    说明:你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。

    输出:你最后获得的最大钱数。

    思考:给定一个初始化投资资金,给定N个项目,想要获得其中最大的收益,并且一次只能做一个项目。这是一个贪心策略的问题,应该在能做的项目中选择收益最大的。

    按照花费的多少放到一个小根堆里面,然后要是小根堆里面的头节点的花费少于给定资金,就将头节点一个个取出来,放到按照收益的大根堆里面。然后做大根堆顶的项目即可。

     并查集实现

    并查集是什么东西?

    它是用来管理元素分组情况的一种数据结构。

    他可以高效进行两个操作:

    1. 查询a,b是否在同一组
    2. 合并a和b所在的组

    萌新可能不知所云,这个结构到底有什么用?

    经分析,并查集效率之高超乎想象,对n个元素的并查集进行一次操作的复杂度低于O(logn)

     

    我们先说并查集是如何实现的:

    也是使用树形结构,但不是二叉树。

    每个元素就是一个结点,每组都是一个树。

    无需关注它的形状,或哪个节点具体在哪个位置。

     

    初始化:

    我们现在有n个结点,也就是n个元素。

     

    合并:

    然后我们就可以合并了,合并方法就是把一个根放到另一颗树的下面,也就是整棵树作为人家的一个子树。

     

    查询:

    查询两个结点是否是同一组,需要知道这两个结点是不是在一棵树上,让他们分别沿着树向根找,如果两个元素最后走到一个根,他们就在一组。

     

    当然,树形结构都存在退化的缺点,对于每种结构,我们都有自己的优化方法,下面我们说明如何避免退化。

    1. 记录每一棵树的高度,合并操作时,高度小的变为高度大的子树即可。
    2. 路径压缩:对于一个节点,只要走到了根节点,就不必再在很深的地方,直接改为连着根即可。进一步优化:其实每一个经过的节点都可以直接连根。

    这样查询的时候就能很快地知道根是谁了。

     

    下面上代码实现:

    和很多树结构一样,我们没必要真的模拟出来,数组中即可。

    int p[MAX_N];//父亲
    int rank[MAX_N];//高度
    //初始化
    void gg(int n)
    {
        for(int i=0;i<n;i++)
        {
            p[i]=i;//父是自己代表是根
            rank[i]=0;
        }
    }
    //查询根
    int find(int x)
    {
        if(p[x]==x)return x;
        return p[x]=find(p[x])//不断把经过的结点连在根
    }
    //判断是否属于同一组
    bool judge(int x,int y)
    {
        return find(x)==find(y);//查询结果一样就在一组
    }
    //合并
    void unite(int x,int y)
    {
        if(x==y)return;
        if(rank[x]<rank[y])p[x]=y;//深度小,放在大的下面
        else
        {
            p[y]=x;
            if(rank[x]=rank[y])rank[x]++;//一样,y放x后,x深度加一
        }
    }

    实现很简单,应用有难度,以后有时间更新题。

    并查集入门三连:HDU1213 POJ1611 POJ2236

    HDU1213

    http://acm.hdu.edu.cn/showproblem.php?pid=1213

    问题描述

    今天是伊格纳修斯的生日。他邀请了很多朋友。现在是晚餐时间。伊格纳修斯想知道他至少需要多少桌子。你必须注意到并非所有的朋友都互相认识,而且所有的朋友都不想和陌生人呆在一起。

    这个问题的一个重要规则是,如果我告诉你A知道B,B知道C,那意味着A,B,C彼此了解,所以他们可以留在一个表中。

    例如:如果我告诉你A知道B,B知道C,D知道E,所以A,B,C可以留在一个表中,D,E必须留在另一个表中。所以Ignatius至少需要2张桌子。

    输入

    输入以整数T(1 <= T <= 25)开始,表示测试用例的数量。然后是T测试案例。每个测试用例以两个整数N和M开始(1 <= N,M <= 1000)。N表示朋友的数量,朋友从1到N标记。然后M行跟随。每一行由两个整数A和B(A!= B)组成,这意味着朋友A和朋友B彼此了解。两个案例之间会有一个空白行。

     

    对于每个测试用例,只输出Ignatius至少需要多少个表。不要打印任何空白。

    样本输入

    2

    5 3

    1 2

    2 3

    4 5

     

    5 1

    2 5

    样本输出

    2

    4

    并查集基础题

    #include<cstdio>
    #include<iostream>
    using namespace std;
    int fa[1005];
    int n,m;
    void init()//初始化
    {
        for(int i=0;i<1005;i++)
            fa[i]=i;
    }
    int find(int x)//寻根
    {
        if(fa[x]!=x)
            fa[x]=find(fa[x]);
        return fa[x];
    }
    void union(int x,int y)//判断、合并
    {
        int a=find(x),b=find(y);
        if(a!=b)
             fa[b]=a;
    }
    int main()
    {
        int t;
        scanf("%d",&t);
        while(t--)
        {
            int a,b,cnt=0;
            scanf("%d%d",&n,&m);
            init();
            for(int i=1;i<=m;i++)//合并
            {
                scanf("%d%d",&a,&b);
                union(a,b);
            }
            for(int i=1;i<=n;i++)//统计
            {
                find(i);
                if(find(i)==i)
                    cnt++;
            }
            printf("%d\n",cnt);
        }
        return 0;
    }
    

    POJ1611

    http://poj.org/problem?id=1611

    描述

    严重急性呼吸系统综合症(SARS)是一种病因不明的非典型肺炎,在2003年3月中旬被认为是一种全球性威胁。为了尽量减少对他人的传播,最好的策略是将嫌疑人与其他嫌疑人分开。 
    在Not-Spreading-Your-Sickness University(NSYSU),有许多学生团体。同一组中的学生经常互相交流,学生可以加入几个小组。为了防止可能的SARS传播,NSYSU收集所有学生组的成员列表,并在其标准操作程序(SOP)中制定以下规则。 
    一旦组中的成员是嫌疑人,该组中的所有成员都是嫌疑人。 
    然而,他们发现,当学生被认定为嫌疑人时,识别所有嫌疑人并不容易。你的工作是编写一个找到所有嫌疑人的程序。

    输入

    输入文件包含几种情况。每个测试用例以一行中的两个整数n和m开始,其中n是学生数,m是组的数量。您可以假设0 <n <= 30000且0 <= m <= 500.每个学生都使用0到n-1之间的唯一整数进行编号,并且最初学生0在所有情况下都被识别为嫌疑人。该行后面是组的m个成员列表,每组一行。每行以整数k开头,表示组中的成员数。在成员数量之后,有k个整数代表该组中的学生。一行中的所有整数由至少一个空格分隔。 


    n = 0且m = 0的情况表示输入结束,无需处理。

     

    对于每种情况,输出一行中的嫌疑人数量。

    样本输入

    100 4
    2 1 2
    5 10 13 11 12 14
    2 0 1
    2 99 2
    200 2
    1 5
    5 1 2 3 4 5
    1 0
    0 0

    样本输出

    4
    1
    1

     

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include <string>
    using namespace std;
    int a[30001],pre[30001];
    int find(int x)//寻根
    {
    	 if(pre[x]==x)
            return x;
        else
            return pre[x]=find(pre[x]);
    }
    void union(int x, int y)//合并
    {
    	int fx = find(x), fy = find(y);
    	if (fx != fy)
    		pre[fy] = fx;
    }
    
    int main()
    {
    	int n,m;
    	while (scanf("%d%d", &n, &m) != EOF && (n || m))
    	{
    		int sum = 0;
    		for (int i = 0; i < n; i++)//初始化
    			pre[i] = i;
    		for (int i = 0; i < m; i++)
    		{
    			int k;
    			scanf("%d", &k);
    			if (k >= 1)
    			{
    				scanf("%d", &a[0]);
    				for (int j = 1; j < k; j++)
    				{
    					scanf("%d", &a[j]);//接收
    					union(a[0], a[j]);//和0号一组
    				}
    			}
    		}
    		for (int i = 0; i < n; i++)//统计
    			if (find(i) ==pre[0])
    				sum++;
    		printf("%d\n", sum);
    	}
    	return 0;
    }
    

     POJ2236

    http://poj.org/problem?id=2236

    描述

    地震发生在东南亚。ACM(亚洲合作医疗团队)已经与膝上电脑建立了无线网络,但是一次意外的余震袭击,网络中的所有计算机都被打破了。计算机一个接一个地修复,网络逐渐开始工作。由于硬件限制,每台计算机只能直接与距离它不远的计算机进行通信。但是,每台计算机都可以被视为两台计算机之间通信的中介,也就是说,如果计算机A和计算机B可以直接通信,或者计算机C可以与A和A进行通信,则计算机A和计算机B可以进行通信。 B. 

    在修复网络的过程中,工作人员可以随时进行两种操作,修复计算机或测试两台计算机是否可以通信。你的工作是回答所有的测试操作。 

    输入

    第一行包含两个整数N和d(1 <= N <= 1001,0 <= d <= 20000)。这里N是计算机的数量,编号从1到N,D是两台计算机可以直接通信的最大距离。在接下来的N行中,每行包含两个整数xi,yi(0 <= xi,yi <= 10000),这是N台计算机的坐标。从第(N + 1)行到输入结束,有一些操作,这些操作是一个接一个地执行的。每行包含以下两种格式之一的操作: 
    1。“O p”(1 <= p <= N),表示修复计算机p。 
    2.“S p q”(1 <= p,q <= N),这意味着测试计算机p和q是否可以通信。 

    输入不会超过300000行。 

    产量

    对于每个测试操作,如果两台计算机可以通信则打印“SUCCESS”,否则打印“FAIL”。

    样本输入

    4 1
    0 1
    0 2
    0 3
    0 4
    O 1
    O 2
    O 4
    S 1 4
    O 3
    S 1 4

    样本输出

    FAIL
    SUCCESS

     思路:对每次修好的电脑对其它已经修好的电脑遍历,如果距离小于等于最大通信距离就将他们合并。

    注意

      1、坐标之后给出的计算机编号都是n+1的。例如O 3,他实际上修理的是编号为2的计算机,因为计算机是从0开始编号的。

      2、比较距离的时候注意要用浮点数比较,否则会WA。

      3、"FAIL"不要写成"FALL"。

      4、字符串输入的时候注意处理好回车,空格等情况。

      5、注意N的范围(1 <= N <= 1001),最大是1001,不是1000。是个小坑,数组开小了可能会错哦。

     

    #include <iostream>
    #include <stdio.h>
    #include <cmath>
    using namespace std;
    
    #define MAXN 1010
    
    int dx[MAXN],dy[MAXN];    //坐标
    int par[MAXN];    //x的父节点
    int repair[MAXN] ={0};
    int n;
    
    void Init()//初始化
    {
        int i;
        for(i=0;i<=n;i++)
            par[i] = i;
    }
    
    int Find(int x)//寻根
    {
        if(par[x]!=x)
            par[x] = Find(par[x]);
        return par[x];
    }
    
    void Union(int x,int y)//合并
    {
        par[Find(x)] = Find(y);
    }
    
    int Abs(int n)//绝对值
    {
        return n>0?n:-n;
    }
    
    double Dis(int a,int b)//坐标
    {
        return sqrt( double(dx[a]-dx[b])*(dx[a]-dx[b]) + (dy[a]-dy[b])*(dy[a]-dy[b]) );
    }
    
    int main()
    {
        int d,i;
    
        //初始化
        scanf("%d%d",&n,&d);
        Init();
    
        //输入坐标
        for(i=0;i<n;i++){
            scanf("%d%d",&dx[i],&dy[i]);
        }
        
        //操作
        char cmd[2];
        int p,q,len=0;
        while(scanf("%s",cmd)!=EOF)
        {
            switch(cmd[0])
            {
                case 'O':
                    scanf("%d",&p);
                    p--;
                    repair[len++] = p;
                    for(i=0;i<len-1;i++)    //遍历所有修过的计算机,看能否联通
                        if( repair[i]!=p && Dis(repair[i],p)<=double(d) )
                            Union(repair[i],p);
                    break;
                case 'S':
                    scanf("%d%d",&p,&q);
                    p--,q--;
                    if(Find(p)==Find(q))    //判断
                        printf("SUCCESS\n");
                    else 
                        printf("FAIL\n");
                default:
                    break;
            }
        }
    
        return 0;
    }

    线段树简单实现

    首先,线段树是一棵满二叉树。(每个节点要么有两个孩子,要么是深度相同的叶子节点)

    每个节点维护某个区间,根维护所有的。

     转存失败重新上传取消 

    如图,区间是二分父的区间。

    当有n个元素,初始化需要o(n)时间,对区间操作需要o(logn)时间。

    下面给出维护区间最小值的思路和代码

    功能:一样的,依旧是查询和改值。

    查询[s,t]之间最小的数。修改某个值。

     

    从下往上,每个节点的值为左右区间较小的那一个即可。

    这算是简单动态规划思想,做到了o(n),因为每个节点就访问一遍,而叶子节点一共n个,所以访问2n次即可。

    如果利用深搜初始化,会到o(nlogn)。

    https://blog.csdn.net/hebtu666/article/details/81777273

    有介绍

    那我们继续说,如何查询。

    不要以为它是二分区间就只能查二分的那些区间,它能查任意区间。

    比如上图,求1-7的最小值,查询1-4,5-6,7-7即可。

    下面说过程:

    递归实现:

    如果要查询的区间和本节点区间没有重合,返回一个特别大的数即可,不要影响其他结果。

    如果要查询的区间完全包含了本节点区间,返回自身的值

    都不满足,对左右儿子做递归,返回较小的值。

     

    如何更新?

    更新ai,就要更新所有包含ai的区间。

    可以从下往上不断更新,把节点的值更新为左右孩子较小的即可。

     

    代码实现和相关注释:

    注:没有具体的初始化,dp思路写过了,实在不想写了

    初始全为INT_MAX

    const int MAX_N=1<<7;
    int n;
    int tree[2*MAX_N-1];
    //初始化
    void gg(int nn)
    {
        n=1;
        while(n<nn)n*=2;//把元素个数变为2的n次方
        for(int i=0;i<2*n-1;i++)tree[i]=INTMAX;//所有值初始化为INTMAX
    }
    
    //查询区间最小值
    int get(int a,int b,int k,int l,int r)//l和r是区间,k是节点下标,求[a,b)最小值
    {
        if(a>=r || b<=l)return INTMAX;//情况1
        if(a<=l || b<=b)return tree[k];//情况2
        int ll=get(a,b,k*2+1,l,(l+r)/2);//以前写过,左孩子公式
        int rr=get(a,b,k*2+2,(l+r)/2,r);//右孩子
        return min(ll,rr);
    }
    
    //更新
    void update(int k,int a)//第k个值更新为a
    {
        //本身
        k+=n-1;//加上前面一堆节点数
        tree[k]=a;
        //开始向上
        while(k>0)
        {
            tree[k]=min(tree[2*k+1],tree[2*k+2]);
            k=(k-1)/2//父的公式,也写过
        }
    }

     树状数组实现

    树状数组能够完成如下操作:

    给一个序列a0-an

    计算前i项和

    对某个值加x

    时间o(logn)

     

    注意:有人觉得前缀和就行了,但是你还要维护啊,改变某个值,一个一个改变前缀和就是o(n)了。

    线段树树状数组的题就是这样,维护一个树,比较容易看出来。

     

     

    线段树:

    https://blog.csdn.net/hebtu666/article/details/82691008

    如果使用线段树,只需要对网址中的实现稍微修改即可。以前维护最小值,现在维护和而已。

    注意:要求只是求出前i项,而并未给定一个区间,那我们就能想出更快速、方便的方法。

    对于任意一个节点,作为右孩子,如果求和时被用到,那它的左兄弟一定也会被用到,那我们就没必要再用右孩子,因为用他们的父就可以了。

    这样一来,我们就可以把所有有孩子全部去掉

    把剩下的节点编号。

     转存失败重新上传取消 

    如图,可以发现一些规律:1,3,5,7,9等奇数,区间长度都为1

    6,10,14等长度为2

    ........................

    如果我们吧编号换成二进制,就能发现,二进制以1结尾的数字区间长度为1,最后有一个零的区间为2,两个零的区间为4.

    我们利用二进制就能很容易地把编号和区间对应起来。

     

    计算前i项和。

    需要把当前编号i的数值加进来,把i最右边的1减掉,直到i变为0.

    二进制最后一个1可以通过i&-i得到。

     

    更新:

    不断把当前位置i加x,把i的二进制最低非零位对应的幂加到i上。

    下面是代码:

    思想想出来挺麻烦,代码实现很简单,我都不知道要注释点啥

    向发明这些东西的大佬们致敬

    int bit[MAX_N+1]
    int n;
    
    int sum(int i)
    {
        int gg=0;
        while(i>0)
        {
            gg+=bit[i];
            i-=i&-i;
        }
        return gg;
    }
    
    void add(int i,int x)
    {
        while(i<=n)
        {
            bit[i]+=x;
            i+=i&-i;
        }
    }

    最大搜索子树

    给定一个二叉树的头结点,返回最大搜索子树的大小。

     

    我们先定义结点:

        public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    分析:

    直接判断每个节点左边小右边大是不对滴

     

    可以暴力判断所有的子树,就不说了。

     

    最大搜索子树可能性:

    第一种可能性,以node为头的结点的最大二叉搜索子树可能来自它左子树;
    第二种可能性,以node为头的结点的最大二叉搜索子树可能来自它右子树;
    第三种可能性,左树整体是搜索二叉树,右树整体也是搜索二叉树,而且左树的头是node.left,右树的头是node.right,且左树的最大值< node.value,右树的最小值 > node.value, 那么以我为头的整棵树都是搜索二叉树;
     

    第三种可能性的判断,需要的信息有:左子树的最大值、右子树的最小值、左子树是不是搜索二叉树、右子树是不是搜索二叉树

    还有左右搜索二叉树的最大深度。

    我们判断了自己,并不知道自己是哪边的子树,我们要返回自己的最大值和最小值。

    这样,定义一个返回类型:

        public static class ReturnType{
    		public int size;//最大搜索子树深度
    		public Node head;//最大搜索子树的根
    		public int min;//子树最小
    		public int max;//子树最大
    		
    		public ReturnType(int a, Node b,int c,int d) {
    			this.size =a;
    			this.head = b;
    			this.min = c;
    			this.max = d;
    		}
    	}

    然后开始写代码:

    注意:

    1)NULL返回深度0,头为NULL,最大值最小值返回系统最大和最小,这样才不会影响别的判断。

    	public static ReturnType process(Node head) {
    		if(head == null) {
    			return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);
    		}
    		
    		Node left = head.left;//取信息
    		ReturnType leftSubTressInfo = process(left);
    		Node right = head.right;
    		ReturnType rightSubTressInfo = process(right);
    		
    		int includeItSelf = 0;
    		if(leftSubTressInfo.head == left //            左子树为搜索树
    				&&rightSubTressInfo.head == right//    右子树为搜索树
    				&& head.value > leftSubTressInfo.max// 左子树最大值小于当前节点
    				&& head.value < rightSubTressInfo.min//右子树最小值大于当前节点
    				) {
    			includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//当前节点为根的二叉树为搜索树
    		}
    		
    		int p1 = leftSubTressInfo.size;
    		int p2 = rightSubTressInfo.size;
    		
    		int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜索树深度
    		
    		Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;
    
    		if(maxSize == includeItSelf) {
    			maxHead = head;
    		}//最大搜索树的根:来自左子树、来自右子树、本身
    		
    		return new ReturnType(
    				maxSize,                                                                     //深度
    				maxHead,                                                                     //根
    				Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value),    //最小
    				Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value));	//最大
    	}

    可以进一步改进:

    空间浪费比较严重

    其实返回值为三个int,一个node,我们可以把三个int合起来,用全局数组记录,函数只返回node(搜索树的根)即可。

    给出完整代码:

    public class BiggestSubBSTInTree {
    
    	public static class Node {
    		public int value;
    		public Node left;
    		public Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    
    	public static Node biggestSubBST(Node head) {
    		int[] record = new int[3]; // 0->size, 1->min, 2->max
    		return posOrder(head, record);
    	}
    	
    	public static class ReturnType{
    		public int size;//最大搜索子树深度
    		public Node head;//最大搜索子树的根
    		public int min;//子树最小
    		public int max;//子树最大
    		
    		public ReturnType(int a, Node b,int c,int d) {
    			this.size =a;
    			this.head = b;
    			this.min = c;
    			this.max = d;
    		}
    	}
    	
    	public static ReturnType process(Node head) {
    		if(head == null) {
    			return new ReturnType(0,null,Integer.MAX_VALUE, Integer.MIN_VALUE);
    		}
    		
    		Node left = head.left;//取信息
    		ReturnType leftSubTressInfo = process(left);
    		Node right = head.right;
    		ReturnType rightSubTressInfo = process(right);
    		
    		int includeItSelf = 0;
    		if(leftSubTressInfo.head == left //            左子树为搜索树
    				&&rightSubTressInfo.head == right//    右子树为搜索树
    				&& head.value > leftSubTressInfo.max// 左子树最大值小于当前节点
    				&& head.value < rightSubTressInfo.min//右子树最小值大于当前节点
    				) {
    			includeItSelf = leftSubTressInfo.size + 1 + rightSubTressInfo.size;//当前节点为根的二叉树为搜索树
    		}
    		
    		int p1 = leftSubTressInfo.size;
    		int p2 = rightSubTressInfo.size;
    		
    		int maxSize = Math.max(Math.max(p1, p2), includeItSelf);//最大搜索树深度
    		
    		Node maxHead = p1 > p2 ? leftSubTressInfo.head : rightSubTressInfo.head;
    		if(maxSize == includeItSelf) {
    			maxHead = head;
    		}//最大搜索树的根:来自左子树、来自右子树、本身
    		
    		return new ReturnType(
    				maxSize,                                                                     //深度
    				maxHead,                                                                     //根
    				Math.min(Math.min(leftSubTressInfo.min,rightSubTressInfo.min),head.value),   //最小
    				Math.max(Math.max(leftSubTressInfo.max,rightSubTressInfo.max),head.value));	 //最大
    	}
    	
    	
    	
    
    	public static Node posOrder(Node head, int[] record) {
    		if (head == null) {
    			record[0] = 0;
    			record[1] = Integer.MAX_VALUE;
    			record[2] = Integer.MIN_VALUE;
    			return null;
    		}
    		int value = head.value;
    		Node left = head.left;
    		Node right = head.right;
    		Node lBST = posOrder(left, record);
    		int lSize = record[0];
    		int lMin = record[1];
    		int lMax = record[2];
    		Node rBST = posOrder(right, record);
    		int rSize = record[0];
    		int rMin = record[1];
    		int rMax = record[2];
    		record[1] = Math.min(rMin, Math.min(lMin, value)); // lmin, value, rmin -> min 
    		record[2] = Math.max(lMax, Math.max(rMax, value)); // lmax, value, rmax -> max
    		if (left == lBST && right == rBST && lMax < value && value < rMin) {
    			record[0] = lSize + rSize + 1;//修改深度
    			return head;                  //返回根
    		}//满足当前构成搜索树的条件
    		record[0] = Math.max(lSize, rSize);//较大深度
    		return lSize > rSize ? lBST : rBST;//返回较大搜索树的根
    	}
    
    	// for test -- print tree
    	public static void printTree(Node head) {
    		System.out.println("Binary Tree:");
    		printInOrder(head, 0, "H", 17);
    		System.out.println();
    	}
    
    	public static void printInOrder(Node head, int height, String to, int len) {
    		if (head == null) {
    			return;
    		}
    		printInOrder(head.right, height + 1, "v", len);
    		String val = to + head.value + to;
    		int lenM = val.length();
    		int lenL = (len - lenM) / 2;
    		int lenR = len - lenM - lenL;
    		val = getSpace(lenL) + val + getSpace(lenR);
    		System.out.println(getSpace(height * len) + val);
    		printInOrder(head.left, height + 1, "^", len);
    	}
    
    	public static String getSpace(int num) {
    		String space = " ";
    		StringBuffer buf = new StringBuffer("");
    		for (int i = 0; i < num; i++) {
    			buf.append(space);
    		}
    		return buf.toString();
    	}
    
    	public static void main(String[] args) {
    
    		Node head = new Node(6);
    		head.left = new Node(1);
    		head.left.left = new Node(0);
    		head.left.right = new Node(3);
    		head.right = new Node(12);
    		head.right.left = new Node(10);
    		head.right.left.left = new Node(4);
    		head.right.left.left.left = new Node(2);
    		head.right.left.left.right = new Node(5);
    		head.right.left.right = new Node(14);
    		head.right.left.right.left = new Node(11);
    		head.right.left.right.right = new Node(15);
    		head.right.right = new Node(13);
    		head.right.right.left = new Node(20);
    		head.right.right.right = new Node(16);
    
    		printTree(head);
    		Node bst = biggestSubBST(head);
    		printTree(bst);
    
    	}
    
    }
    

    morris遍历

    通常,实现二叉树的前序(preorder)、中序(inorder)、后序(postorder)遍历有两个常用的方法:一是递归(recursive),二是使用栈实现的迭代版本(stack+iterative)。这两种方法都是O(n)的空间复杂度(递归本身占用stack空间或者用户自定义的stack)。

    本文介绍空间O(1)的遍历方法。

    上次文章讲到,我们经典递归遍历其实有三次访问当前节点的机会,就看你再哪次进行操作,而分成了三种遍历。

    https://blog.csdn.net/hebtu666/article/details/82853988

    morris有两次访问节点的机会。

    它省空间的原理是利用了大量叶子节点的没有用的空间,记录之前的节点,做到了返回之前节点这件事情。

    我们不说先序中序后序,先说morris遍历的原则:

    1、如果没有左孩子,继续遍历右子树

    2、如果有左孩子,找到左子树最右节点。

        1)如果最右节点的右指针为空(说明第一次遇到),把它指向当前节点,当前节点向左继续处理。

        2)如果最右节点的右指针不为空(说明它指向之前结点),把右指针设为空,当前节点向右继续处理。

     

    这就是morris遍历。

    请手动模拟深度至少为3的树的morris遍历来熟悉流程。

     

    先看代码:

    定义结点:

    	public static class Node {
    		public int value;
    		Node left;
    		Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}

    先序:

     (完全按规则写就好。)

    //打印时机(第一次遇到):发现左子树最右的孩子右指针指向空,或无左子树。
    	public static void morrisPre(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					System.out.print(cur1.value + " ");
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}
    			} else {
    				System.out.print(cur1.value + " ");
    			}
    			cur1 = cur1.right;
    		}
    		System.out.println();
    	}

    morris在发表文章时只写出了中序遍历。而先序遍历只是打印时机不同而已,所以后人改进出了先序遍历。至于后序,是通过打印所有的右边界来实现的:对每个有边界逆序,打印,再逆序回去。注意要原地逆序,否则我们morris遍历的意义也就没有了。

    完整代码: 

    public class MorrisTraversal {
    
    	
    	
    	public static void process(Node head) {
    		if(head == null) {
    			return;
    		}
    		
    		// 1
    		//System.out.println(head.value);
    		
    		
    		process(head.left);
    		
    		// 2
    		//System.out.println(head.value);
    		
    		
    		process(head.right);
    		
    		// 3
    		//System.out.println(head.value);
    	}
    	
    	
    	public static class Node {
    		public int value;
    		Node left;
    		Node right;
    
    		public Node(int data) {
    			this.value = data;
    		}
    	}
    //打印时机:向右走之前
    	public static void morrisIn(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;//当前节点
    		Node cur2 = null;//最右
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			//左孩子不为空
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}//找到最右
    				//右指针为空,指向cur1,cur1向左继续
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}//右指针不为空,设为空
    			}
    			System.out.print(cur1.value + " ");
    			cur1 = cur1.right;
    		}
    		System.out.println();
    	}
    //打印时机(第一次遇到):发现左子树最右的孩子右指针指向空,或无左子树。
    	public static void morrisPre(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					System.out.print(cur1.value + " ");
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    				}
    			} else {
    				System.out.print(cur1.value + " ");
    			}
    			cur1 = cur1.right;
    		}
    		System.out.println();
    	}
    //逆序打印所有右边界
    	public static void morrisPos(Node head) {
    		if (head == null) {
    			return;
    		}
    		Node cur1 = head;
    		Node cur2 = null;
    		while (cur1 != null) {
    			cur2 = cur1.left;
    			if (cur2 != null) {
    				while (cur2.right != null && cur2.right != cur1) {
    					cur2 = cur2.right;
    				}
    				if (cur2.right == null) {
    					cur2.right = cur1;
    					cur1 = cur1.left;
    					continue;
    				} else {
    					cur2.right = null;
    					printEdge(cur1.left);
    				}
    			}
    			cur1 = cur1.right;
    		}
    		printEdge(head);
    		System.out.println();
    	}
    //逆序打印
    	public static void printEdge(Node head) {
    		Node tail = reverseEdge(head);
    		Node cur = tail;
    		while (cur != null) {
    			System.out.print(cur.value + " ");
    			cur = cur.right;
    		}
    		reverseEdge(tail);
    	}
    //逆序(类似链表逆序)
    	public static Node reverseEdge(Node from) {
    		Node pre = null;
    		Node next = null;
    		while (from != null) {
    			next = from.right;
    			from.right = pre;
    			pre = from;
    			from = next;
    		}
    		return pre;
    	}
    	public static void main(String[] args) {
    		Node head = new Node(4);
    		head.left = new Node(2);
    		head.right = new Node(6);
    		head.left.left = new Node(1);
    		head.left.right = new Node(3);
    		head.right.left = new Node(5);
    		head.right.right = new Node(7);
    
    		morrisIn(head);
    		morrisPre(head);
    		morrisPos(head);
    	}
    
    }

    最小生成树

     

    问题提出:
        要在n个城市间建立通信联络网。顶点:表示城市,权:城市间通信线路的花费代价。希望此通信网花费代价最小。
    问题分析:
        答案只能从生成树中找,因为要做到任何两个城市之间有线路可达,通信网必须是连通的;但对长度最小的要求可以知道网中显然不能有圈,如果有圈,去掉一条边后,并不破坏连通性,但总代价显然减少了,这与总代价最小的假设是矛盾的。
    结论:
        希望找到一棵生成树,它的每条边上的权值之和(即建立该通信网所需花费的总代价)最小 —— 最小代价生成树。
        构造最小生成树的算法很多,其中多数算法都利用了一种称之为 MST 的性质。
        MST 性质:设 N = (V, E)  是一个连通网,U是顶点集 V的一个非空子集。若边 (u, v) 是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边 (u, v) 的最小生成树。


    (1)普里姆 (Prim) 算法

    算法思想: 
        ①设 N=(V, E)是连通网,TE是N上最小生成树中边的集合。
        ②初始令 U={u_0}, (u_0∈V), TE={ }。
        ③在所有u∈U,u∈U-V的边(u,v)∈E中,找一条代价最小的边(u_0,v_0 )。
        ④将(u_0,v_0 )并入集合TE,同时v_0并入U。
        ⑤重复上述操作直至U = V为止,则 T=(V,TE)为N的最小生成树。

     
    代码实现:

    void MiniSpanTree_PRIM(MGraph G,VertexType u)
        //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。
        //记录从顶点集U到V-U的代价最小的边的辅助数组定义;
        //closedge[j].lowcost表示在集合U中顶点与第j个顶点对应最小权值
    {
        int k, j, i;
        k = LocateVex(G,u);
        for (j = 0; j < G.vexnum; ++j)    //辅助数组的初始化
            if(j != k)
            {
                closedge[j].adjvex = u;
                closedge[j].lowcost = G.arcs[k][j].adj;    
    //获取邻接矩阵第k行所有元素赋给closedge[j!= k].lowcost
            }
        closedge[k].lowcost = 0;        
    //初始,U = {u};  
        PrintClosedge(closedge,G.vexnum);
        for (i = 1; i < G.vexnum; ++i)    \
    //选择其余G.vexnum-1个顶点,因此i从1开始循环
        {
            k = minimum(G.vexnum,closedge);        
    //求出最小生成树的下一个结点:第k顶点
            PrintMiniTree_PRIM(G, closedge, k);     //输出生成树的边
            closedge[k].lowcost = 0;                //第k顶点并入U集
            PrintClosedge(closedge,G.vexnum);
            for(j = 0;j < G.vexnum; ++j)
            {                                           
                if(G.arcs[k][j].adj < closedge[j].lowcost)    
    //比较第k个顶点和第j个顶点权值是否小于closedge[j].lowcost
                {
                    closedge[j].adjvex = G.vexs[k];//替换closedge[j]
                    closedge[j].lowcost = G.arcs[k][j].adj;
                    PrintClosedge(closedge,G.vexnum);
                }
            }
        }
    }


    (2)克鲁斯卡尔 (Kruskal) 算法

    算法思想: 
        ①设连通网  N = (V, E ),令最小生成树初始状态为只有n个顶点而无边的非连通图,T=(V, { }),每个顶点自成一个连通分量。
        ②在 E 中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
    ③依此类推,直至 T 中所有顶点都在同一连通分量上为止。
          
        最小生成树可能不惟一!

     

    拓扑排序

     

    (1)有向无环图

        无环的有向图,简称 DAG (Directed Acycline Graph) 图。
     
    有向无环图在工程计划和管理方面的应用:除最简单的情况之外,几乎所有的工程都可分为若干个称作“活动”的子工程,并且这些子工程之间通常受着一定条件的约束,例如:其中某些子工程必须在另一些子工程完成之后才能开始。
    对整个工程和系统,人们关心的是两方面的问题: 
    ①工程能否顺利进行; 
    ②完成整个工程所必须的最短时间。

    对应到有向图即为进行拓扑排序和求关键路径。 
    AOV网: 
        用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
    例如:排课表
          
    AOV网的特点:
    ①若从i到j有一条有向路径,则i是j的前驱;j是i的后继。
    ②若< i , j >是网中有向边,则i是j的直接前驱;j是i的直接后继。
    ③AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。


    问题:    
        问题:如何判别 AOV 网中是否存在回路?
        检测 AOV 网中是否存在环方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。


    拓扑排序的方法:
        ①在有向图中选一个没有前驱的顶点且输出之。
        ②从图中删除该顶点和所有以它为尾的弧。
        ③重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。
            
        一个AOV网的拓扑序列不是唯一的!
    代码实现:

    Status TopologicalSort(ALGraph G)
        //有向图G采用邻接表存储结构。
        //若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR.
        //输出次序按照栈的后进先出原则,删除顶点,输出遍历
    {
        SqStack S;
        int i, count;
        int *indegree1 = (int *)malloc(sizeof(int) * G.vexnum);
        int indegree[12] = {0};
        FindInDegree(G, indegree);    //求个顶点的入度下标从0开始
        InitStack(&S);
        PrintStack(S);
        for(i = 0; i < G.vexnum; ++i)
            if(!indegree[i])        //建0入度顶点栈S
                push(&S,i);        //入度为0者进栈
        count = 0;                //对输出顶点计数
        while (S.base != S.top)
        {
            ArcNode* p;
            pop(&S,&i);
            VisitFunc(G,i);//第i个输出栈顶元素对应的顶点,也就是最后进来的顶点    
            ++count;          //输出i号顶点并计数
            for(p = G.vertices[i].firstarc; p; p = p->nextarc)
            {    //通过循环遍历第i个顶点的表结点,将表结点中入度都减1
                int k = p->adjvex;    //对i号顶点的每个邻接点的入度减1
                if(!(--indegree[k]))
                    push(&S,k);        //若入度减为0,则入栈
            }//for
        }//while
        if(count < G.vexnum)
        {
            printf("\n该有向图有回路!\n");
            return ERROR;    //该有向图有回路
        }
        else
        {
            printf("\n该有向图没有回路!\n");
            return OK;
        }
    }


    关键路径

        把工程计划表示为有向图,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。称这种有向图为边表示活动的网,简称为 AOE网 (Activity On Edge)。
    例如:
    设一个工程有11项活动,9个事件。
    事件v_1——表示整个工程开始(源点) 
    事件v_9——表示整个工程结束(汇点)

     
    对AOE网,我们关心两个问题:  
    ①完成整项工程至少需要多少时间? 
    ②哪些活动是影响工程进度的关键?
    关键路径——路径长度最长的路径。
    路径长度——路径上各活动持续时间之和。
    v_i——表示事件v_i的最早发生时间。假设开始点是v_1,从v_1到〖v�i〗的最长路径长度。ⅇ(ⅈ)——表示活动a_i的最早发生时间。
    l(ⅈ)——表示活动a_i最迟发生时间。在不推迟整个工程完成的前提下,活动a_i最迟必须开始进行的时间。
    l(ⅈ)-ⅇ(ⅈ)意味着完成活动a_i的时间余量。
    我们把l(ⅈ)=ⅇ(ⅈ)的活动叫做关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程进度。
        例如上图中网,从从v_1到v_9的最长路径是(v_1,v_2,v_5,v_8,ν_9 ),路径长度是18,即ν_9的最迟发生时间是18。而活动a_6的最早开始时间是5,最迟开始时间是8,这意味着:如果a_6推迟3天或者延迟3天完成,都不会影响整个工程的完成。因此,分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的工效,缩短整个工期。
        由上面介绍可知:辨别关键活动是要找l(ⅈ)=ⅇ(ⅈ)的活动。为了求ⅇ(ⅈ)和l(ⅈ),首先应求得事件的最早发生时间vⅇ(j)和最迟发生时间vl(j)。如果活动a_i由弧〈j,k〉表示,其持续时间记为dut(〈j,k〉),则有如下关系:
    ⅇ(ⅈ)= vⅇ(j)
    l(ⅈ)=vl(k)-dut(〈j,k〉)
        求vⅇ(j)和vl(j)需分两步进行:
    第一步:从vⅇ(0)=0开始向前递推
    vⅇ(j)=Max{vⅇ(i)+dut(〈j,k〉)}   〈i,j〉∈T,j=1,2,…,n-1
    其中,T是所有以第j个顶点为头的弧的集合。
    第二步:从vl(n-1)=vⅇ(n-1)起向后递推
    vl(i)=Min{vl(j)-dut(〈i,j〉)}  〈i,j〉∈S,i=n-2,…,0
    其中,S是所有以第i个顶点为尾的弧的集合。
    下面我们以上图AOE网为例,先求每个事件v_i的最早发生时间,再逆向求每个事件对应的最晚发生时间。再求每个活动的最早发生时间和最晚发生时间,如下面表格:
              
    在活动的统计表中,活动的最早发生时间和最晚发生时间相等的,就是关键活动


    关键路径的讨论:

    ①若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。      如:a11、a10、a8、a7。 
    ②如果一个活动处于所有的关键路径上,则提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4。
    ③处于所有关键路径上的活动完成时间不能缩短太多,否则会使原关键路径变成非关键路径。这时必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。

    关键路径算法实现:

    int CriticalPath(ALGraph G)
    {    //因为G是有向网,输出G的各项关键活动
        SqStack T;
        int i, j;    ArcNode* p;
        int k , dut;
        if(!TopologicalOrder(G,T))
            return 0;
        int vl[VexNum];
        for (i = 0; i < VexNum; i++)
            vl[i] = ve[VexNum - 1];        //初始化顶点事件的最迟发生时间
        while (T.base != T.top)            //按拓扑逆序求各顶点的vl值
        {
     
            for(pop(&T, &j), p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                k = p->adjvex;    dut = *(p->info);    //dut<j, k>
                if(vl[k] - dut < vl[j])
                    vl[j] = vl[k] - dut;
            }//for
        }//while
        for(j = 0; j < G.vexnum; ++j)    //求ee,el和关键活动
        {
            for (p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                int ee, el;        char tag;
                k = p->adjvex;    dut = *(p->info);
                ee = ve[j];    el = vl[k] - dut;
                tag = (ee == el) ? '*' : ' ';
                PrintCriticalActivity(G,j,k,dut,ee,el,tag);
            }
        }
        return 1;
    }

    最短路

     

    最短路

        典型用途:交通网络的问题——从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?
     
        交通网络用有向网来表示:顶点——表示城市,弧——表示两个城市有路连通,弧上的权值——表示两城市之间的距离、交通费或途中所花费的时间等。
        如何能够使一个城市到另一个城市的运输时间最短或运费最省?这就是一个求两座城市间的最短路径问题。
        问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n - 1条边。
       常见最短路径问题:单源点最短路径、所有顶点间的最短路径
    (1)如何求得单源点最短路径?
        穷举法:将源点到终点的所有路径都列出来,然后在其中选最短的一条。但是,当路径特别多时,特别麻烦;没有规律可循。
        迪杰斯特拉(Dijkstra)算法:按路径长度递增次序产生各顶点的最短路径。
    路径长度最短的最短路径的特点:
        在此路径上,必定只含一条弧 <v_0, v_1>,且其权值最小。由此,只要在所有从源点出发的弧中查找权值最小者。
    下一条路径长度次短的最短路径的特点:
    ①、直接从源点到v_2<v_0, v_2>(只含一条弧);
    ②、从源点经过顶点v_1,再到达v_2<v_0, v_1>,<v_1, v_2>(由两条弧组成)
    再下一条路径长度次短的最短路径的特点:
        有以下四种情况:
        ①、直接从源点到v_3<v_0, v_3>(由一条弧组成);
        ②、从源点经过顶点v_1,再到达v_3<v_0, v_1>,<v_1, v_3>(由两条弧组成);
        ③、从源点经过顶点v_2,再到达v_3<v_0, v_2>,<v_2, v_3>(由两条弧组成);
        ④、从源点经过顶点v_1  ,v_2,再到达v_3<v_0, v_1>,<v_1, v_2>,<v_2, v_3>(由三条弧组成);
    其余最短路径的特点:    
        ①、直接从源点到v_i<v_0, v_i>(只含一条弧);
        ②、从源点经过已求得的最短路径上的顶点,再到达v_i(含有多条弧)。
    Dijkstra算法步骤:
        初始时令S={v_0},  T={其余顶点}。T中顶点对应的距离值用辅助数组D存放。
        D[i]初值:若<v_0, v_i>存在,则为其权值;否则为∞。 
        从T中选取一个其距离值最小的顶点v_j,加入S。对T中顶点的距离值进行修改:若加进v_j作中间顶点,从v_0到v_i的距离值比不加 vj 的路径要短,则修改此距离值。
        重复上述步骤,直到 S = V 为止。

    算法实现:

    void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D)
    { // 用Dijkstra算法求有向网 G 的 v0 顶点到其余顶点v的最短路径P[v]及带权长度D[v]。
        // 若P[v][w]为TRUE,则 w 是从 v0 到 v 当前求得最短路径上的顶点。  P是存放最短路径的矩阵,经过顶点变成TRUE
        // final[v]为TRUE当且仅当 v∈S,即已经求得从v0到v的最短路径。
        int v,w,i,j,min;
        Status final[MAX_VERTEX_NUM];
        for(v = 0 ;v < G.vexnum ;++v)
        {
            final[v] = FALSE;
            D[v] = G.arcs[v0][v].adj;        //将顶点数组中下标对应是 v0 和 v的距离给了D[v]
            for(w = 0;w < G.vexnum; ++w)
                P[v][w] = FALSE;            //设空路径
            if(D[v] < INFINITY)
            {
                P[v][v0] = TRUE;
                P[v][v] = TRUE;
            }
        }
        D[v0]=0;
        final[v0]= TRUE; /* 初始化,v0顶点属于S集 */
        for(i = 1;i < G.vexnum; ++i) /* 其余G.vexnum-1个顶点 */
        { /* 开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集 */
            min = INFINITY; /* 当前所知离v0顶点的最近距离 */
            for(w = 0;w < G.vexnum; ++w)
                if(!final[w]) /* w顶点在V-S中 */
                    if(D[w] < min)
                    {
                        v = w;
                        min = D[w];
                    } /* w顶点离v0顶点更近 */
                    final[v] = TRUE; /* 离v0顶点最近的v加入S集 */
                    for(w = 0;w < G.vexnum; ++w) /* 更新当前最短路径及距离 */
                    {
                        if(!final[w] && min < INFINITY && G.arcs[v][w].adj < INFINITY && (min + G.arcs[v][w].adj < D[w]))
                        { /* 修改D[w]和P[w],w∈V-S */
                            D[w] = min + G.arcs[v][w].adj;
                            for(j = 0;j < G.vexnum;++j)
                                P[w][j] = P[v][j];
                            P[w][w] = TRUE;
                        }
                    }
        }
    }

    简单迷宫问题

    迷宫实验是取自心理学的一个古典实验。在该实验中,把一只老鼠从一个无顶大盒子的门放入,在盒子中设置了许多墙,对行进方向形成了多处阻挡。盒子仅有一个出口,在出口处放置一块奶酪,吸引老鼠在迷宫中寻找道路以到达出口。对同一只老鼠重复进行上述实验,一直到老鼠从入口到出口,而不走错一步。老鼠经过多次试验终于得到它学习走通迷宫的路线。设计一个计算机程序对任意设定的迷宫,求出一条从入口到出口的通路,或得出没有通路的结论。
    数组元素值为1表示该位置是墙壁,不能通行;元素值为0表示该位置是通路。假定从mg[1][1]出发,出口位于mg[n][m]

    用一种标志在二维数组中标出该条通路,并在屏幕上输出二维数组。

    m=[[1,1,1,0,1,1,1,1,1,1],
       [1,0,0,0,0,0,0,0,1,1],
       [1,0,1,1,1,1,1,0,0,1],
       [1,0,1,0,0,0,0,1,0,1],
       [1,0,1,0,1,1,0,0,0,1],
       [1,0,0,1,1,0,1,0,1,1],
       [1,1,1,1,0,0,0,0,1,1],
       [1,0,0,0,0,1,1,1,0,0],
       [1,0,1,1,0,0,0,0,0,1],
       [1,1,1,1,1,1,1,1,1,1]]
    sta1=0;sta2=3;fsh1=7;fsh2=9;success=0
    def LabyrinthRat():
        print('显示迷宫:')
        for i in range(len(m)):print(m[i])
        print('入口:m[%d][%d]:出口:m[%d][%d]'%(sta1,sta2,fsh1,fsh2))
        if (visit(sta1,sta2))==0:	print('没有找到出口')
        else:
            print('显示路径:')
            for i in range(10):print(m[i])
    def visit(i,j):
        m[i][j]=2
        global success
        if(i==fsh1)and(j==fsh2): success=1
        if(success!=1)and(m[i-1][j]==0): visit(i-1,j)
        if(success!=1)and(m[i+1][j]==0): visit(i+1,j)
        if(success!=1)and(m[i][j-1]==0): visit(i,j-1)
        if(success!=1)and(m[i][j+1]==0): visit(i,j+1)
        if success!=1: m[i][j]=3
        return success
    LabyrinthRat()

    深搜DFS\广搜BFS 

    首先,不管是BFS还是DFS,由于时间和空间的局限性,它们只能解决数据量比较小的问题。

    深搜,顾名思义,它从某个状态开始,不断的转移状态,直到无法转移,然后退回到上一步的状态,继续转移到其他状态,不断重复,直到找到最终的解。从实现上来说,栈结构是后进先出,可以很好的保存上一步状态并利用。所以根据深搜和栈结构的特点,深度优先搜索利用递归函数(栈)来实现,只不过这个栈是系统帮忙做的,不太明显罢了。

     

    广搜和深搜的搜索顺序不同,它是先搜索离初始状态比较近的状态,搜索顺序是这样的:初始状态---------->一步能到的状态--------->两步能到的状态......从实现上说,它是通过队列实现的,并且是我们自己做队列。一般解决最短路问题,因为第一个搜到的一定是最短路。

    下面通过两道简单例题简单的入个门。

    深搜例题

    poj2386

    http://poj.org/problem?id=2386

    题目大意:上下左右斜着挨着都算一个池子,看图中有几个池子。

    W........WW.
    .WWW.....WWW
    ....WW...WW.
    .........WW.
    .........W..
    ..W......W..
    .W.W.....WW.
    W.W.W.....W.
    .W.W......W.
    ..W.......W.例如本图就是有三个池子

    采用深度优先搜索,从任意的w开始,不断把邻接的部分用'.'代替,1次DFS后与初始这个w连接的所有w就全都被替换成'.',因此直到图中不再存在W为止。

    核心代码:

    char field[maxn][maxn];//图
    int n,m;长宽
    void dfs(int x,int y)
    {
        field[x][y]='.';//先做了标记
        //循环遍历八个方向
        for(int dx=-1;dx<=1;dx++){
            for(int dy=-1;dy<=1;dy++){
                int nx=x+dx,ny=y+dy;
                //判断(nx,ny)是否在园子里,以及是否有积水
                if(0<=nx&&nx<n&&0<=ny&&ny<m&&field[nx][ny]=='W'){
                    dfs(nx,ny);
                }
            }
        }
    }
    void solve()
    {
        int res=0;
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(field[i][j]=='W'){
                    //从有积水的地方开始搜
                    dfs(i,j);
                    res++;//搜几次就有几个池子
                }
            }
        }
        printf("%d\n",res);
    }

    广搜例题:

    迷宫的最短路径

      给定一个大小为N×M的迷宫。迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四个的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。(N,M≤100)('#', '.' , 'S', 'G'分别表示墙壁、通道、起点和终点)

    输入:

    10 10

    #S######.#
    ......#..#
    .#.##.##.#
    .#........
    ##.##.####
    ....#....#
    .#######.#
    ....#.....
    .####.###.
    ....#...G#

    输出:

    22

    小白书上部分代码:

    typedef pair<int, int> P;
    char maze[maxn][maxn];
    int n, m, sx, sy, gx, gy,d[maxn][maxn];//到各个位置的最短距离的数组
    int dx[4] = { 1,0,-1,0 }, dy[4]= { 0,1,0,-1 };//4个方向移动的向量
    int bfs()//求从(sx,sy)到(gx,gy)的最短距离,若无法到达则是INF
    {
        queue<P> que; 
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                d[i][j] = INF;//所有的位置都初始化为INF
        que.push(P(sx, sy));//将起点加入队列中
        d[sx][sy] = 0;//并把起点的距离设置为0
        while (que.size())//不断循环直到队列的长度为0
        {
            P p = que.front();// 从队列的最前段取出元素
            que.pop();//删除该元素
            if (p.first == gx&&p.second == gy)//是终点结束
                break;
            for (int i = 0; i < 4; i++)//四个方向的循环
            {
                int nx = p.first + dx[i],ny = p.second + dy[i];//移动后的位置标记为(nx,ny)
                if (0 <= nx&&nx < n && 0 <= ny&&ny < m&&maze[nx][ny] != '#'&&d[nx][ny] == INF)//判断是否可以移动以及是否访问过(即d[nx][ny]!=INF)
                {
                    que.push(P(nx, ny));//可以移动,添加到队列
                    d[nx][ny] = d[p.first][p.second] + 1;//到该位置的距离为到p的距离+1
                }
            }
        }
        return d[gx][gy];
    }

    经典了两个题结束了,好题链接持续更新。。。。。。

     皇后问题

     

    八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n1×n1,而皇后个数也变成n2。而且仅当 n2 ≥ 1 或 n1 ≥ 4 时问题有解。

    皇后问题是非常著名的问题,作为一个棋盘类问题,毫无疑问,用暴力搜索的方法来做是一定可以得到正确答案的,但在有限的运行时间内,我们很难写出速度可以忍受的搜索,部分棋盘问题的最优解不是搜索,而是动态规划,某些棋盘问题也很适合作为状态压缩思想的解释例题。

    进一步说,皇后问题可以用人工智能相关算法和遗传算法求解,可以用多线程技术缩短运行时间。本文不做讨论。

    (本文不展开讲状态压缩,以后再说)

     

    一般思路:

     

    N*N的二维数组,在每一个位置进行尝试,在当前位置上判断是否满足放置皇后的条件(这一点的行、列、对角线上,没有皇后)。

     

    优化1:

     

    既然知道多个皇后不能在同一行,我们何必要在同一行的不同位置放多个来尝试呢?

    我们生成一维数组record,record[i]表示第i行的皇后放在了第几列。对于每一行,确定当前record值即可,因为每行只能且必须放一个皇后,放了一个就无需继续尝试。那么对于当前的record[i],查看record[0...i-1]的值,是否有j = record[k](同列)、|record[k] - j| = | k-i |(同一斜线)的情况。由于我们的策略,无需检查行(每行只放一个)。

    public class NQueens {
    	public static int num1(int n) {
    		if (n < 1) {
    			return 0;
    		}
    		int[] record = new int[n];
    		return process1(0, record, n);
    	}
    	public static int process1(int i, int[] record, int n) {
    		if (i == n) {
    			return 1;
    		}
    		int res = 0;
    		for (int j = 0; j < n; j++) {
    			if (isValid(record, i, j)) {
    				record[i] = j;
    				res += process1(i + 1, record, n);
    			}
    		}//对于当前行,依次尝试每列
    		return res;
    	}
    //判断当前位置是否可以放置
    	public static boolean isValid(int[] record, int i, int j) {
    		for (int k = 0; k < i; k++) {
    			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
    				return false;
    			}
    		}
    		return true;
    	}
    	public static void main(String[] args) {
    		int n = 8;
    		System.out.println(num1(n));
    	}
    }
    

    优化2:

     

    分析:棋子对后续过程的影响范围:本行、本列、左右斜线。

    黑色棋子影响区域为红色

    本行影响不提,根据优化一已经避免

    本列影响,一直影响D列,直到第一行在D放棋子的所有情况结束。

     

    左斜线:每向下一行,实际上对当前行的影响区域就向左移动

    比如:

    尝试第二行时,黑色棋子影响的是我们的第三列;

    尝试第三行时,黑色棋子影响的是我们的第二列;

    尝试第四行时,黑色棋子影响的是我们的第一列;

    尝试第五行及以后几行,黑色棋子对我们并无影响。

     

    右斜线则相反:

    随着行序号增加,影响的列序号也增加,直到影响的列序号大于8就不再影响。

     

    我们对于之前棋子影响的区域,可以用二进制数字来表示,比如:

    每一位,用01代表是否影响。

    比如上图,对于第一行,就是00010000

    尝试第二行时,数字变为00100000

    第三行:01000000

    第四行:10000000

     

    对于右斜线的数字,同理:

    第一行00010000,之后向右移:00001000,00000100,00000010,00000001,直到全0不影响。

     

    同理,我们对于多行数据,也同样可以记录了

    比如在第一行我们放在了第四列:

    第二行放在了G列,这时左斜线记录为00100000(第一个棋子的影响)+00000010(当前棋子的影响)=00100010。

    到第三行数字继续左移:01000100,然后继续加上我们的选择,如此反复。

     

    这样,我们对于当前位置的判断,其实可以通过左斜线变量、右斜线变量、列变量,按位或运算求出(每一位中,三个数有一个是1就不能再放)。

    具体看代码:

    注:怎么排版就炸了呢。。。贴一张图吧

    public class NQueens {
    	public static int num2(int n) {
    		// 因为本方法中位运算的载体是int型变量,所以该方法只能算1~32皇后问题
    		// 如果想计算更多的皇后问题,需使用包含更多位的变量
    		if (n < 1 || n > 32) {
    			return 0;
    		}
    		int upperLim = n == 32 ? -1 : (1 << n) - 1;
            //upperLim的作用为棋盘大小,比如8皇后为00000000 00000000 00000000 11111111
            //32皇后为11111111 11111111 11111111 11111111
    		return process2(upperLim, 0, 0, 0);
    	}
    
    	public static int process2(int upperLim, int colLim, int leftDiaLim,
    			int rightDiaLim) {
    		if (colLim == upperLim) {
    			return 1;
    		}
    		int pos = 0;            //pos:所有的合法位置
    		int mostRightOne = 0;   //所有合法位置的最右位置
    
            //所有记录按位或之后取反,并与全1按位与,得出所有合法位置
    		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
    		int res = 0;//计数
    		while (pos != 0) {
    			mostRightOne = pos & (~pos + 1);//取最右的合法位置
    			pos = pos - mostRightOne;       //去掉本位置并尝试
    			res += process2(
                         upperLim,                             //全局
                         colLim | mostRightOne,                //列记录
                         //之前列+本位置
    					(leftDiaLim | mostRightOne) << 1,      //左斜线记录
                         //(左斜线变量+本位置)左移             
    					(rightDiaLim | mostRightOne) >>> 1);   //右斜线记录
                         //(右斜线变量+本位置)右移(高位补零)
    		}
    		return res;
    	}
    
    	public static void main(String[] args) {
    		int n = 8;
    		System.out.println(num2(n));
    	}
    }
    

    完整测试代码:

    32皇后:结果/时间

    暴力搜:时间就太长了,懒得测。。。

    public class NQueens {
    
    	public static int num1(int n) {
    		if (n < 1) {
    			return 0;
    		}
    		int[] record = new int[n];
    		return process1(0, record, n);
    	}
    
    	public static int process1(int i, int[] record, int n) {
    		if (i == n) {
    			return 1;
    		}
    		int res = 0;
    		for (int j = 0; j < n; j++) {
    			if (isValid(record, i, j)) {
    				record[i] = j;
    				res += process1(i + 1, record, n);
    			}
    		}
    		return res;
    	}
    
    	public static boolean isValid(int[] record, int i, int j) {
    		for (int k = 0; k < i; k++) {
    			if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
    				return false;
    			}
    		}
    		return true;
    	}
    
    	public static int num2(int n) {
    		if (n < 1 || n > 32) {
    			return 0;
    		}
    		int upperLim = n == 32 ? -1 : (1 << n) - 1;
    		return process2(upperLim, 0, 0, 0);
    	}
    
    	public static int process2(int upperLim, int colLim, int leftDiaLim,
    			int rightDiaLim) {
    		if (colLim == upperLim) {
    			return 1;
    		}
    		int pos = 0;
    		int mostRightOne = 0;
    		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
    		int res = 0;
    		while (pos != 0) {
    			mostRightOne = pos & (~pos + 1);
    			pos = pos - mostRightOne;
    			res += process2(upperLim, colLim | mostRightOne,
    					(leftDiaLim | mostRightOne) << 1,
    					(rightDiaLim | mostRightOne) >>> 1);
    		}
    		return res;
    	}
    
    	public static void main(String[] args) {
    		int n = 32;
    
    		long start = System.currentTimeMillis();
    		System.out.println(num2(n));
    		long end = System.currentTimeMillis();
    		System.out.println("cost time: " + (end - start) + "ms");
    
    		start = System.currentTimeMillis();
    		System.out.println(num1(n));
    		end = System.currentTimeMillis();
    		System.out.println("cost time: " + (end - start) + "ms");
    	}
    }
    

    二叉搜索树实现

    本文给出二叉搜索树介绍和实现

     

    首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

     

    那这个结构有什么有用呢?

    首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

    基本操作:

    1、插入某个数值

    2、查询是否包含某个数值

    3、删除某个数值

     

    根据实现不同,还可以实现其他很多种操作。

     

    实现思路思路:

    前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

    而删除稍微复杂一些,有下面这几种情况:

    1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

    2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

    3、以上都不满足,就把左儿子子孙中最大节点提上来。

     

    当然,反过来也是成立的,比如右儿子子孙中最小的节点。

     

    下面来叙述为什么可以这么做。

    下图中A为待删除节点。

    第一种情况:

     

    1、去掉A,把c提上来,c也是小于x的没问题。

    2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

     

    第二种情况

     

    3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

    4、同理

     

    第三种情况

     

    5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

    因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

    而黑点小于A,也就小于c,所以可以让c当右孩子

    大概证明就这样。。

    下面我们用代码实现并通过注释理解

    上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

    定义

    struct node
    {
        int val;//数据
        node *lch,*rch;//左右孩子
    };

    插入

     node *insert(node *p,int x)
     {
         if(p==NULL)//直到空就创建节点
         {
             node *q=new node;
             q->val=x;
             q->lch=q->rch=NULL;
             return p;
         }
         if(x<p->val)p->lch=insert(p->lch,x);
         else p->lch=insert(p->rch,x);
         return p;//依次返回自己,让上一个函数执行。
     }

    查找

     bool find(node *p,int x)
     {
         if(p==NULL)return false;
         else if(x==p->val)return true;
         else if(x<p->val)return find(p->lch,x);
         else return find(p->rch,x);
     }

    删除

     node *remove(node *p,int x)
     {
          if(p==NULL)return NULL;
          else if(x<p->val)p->lch=remove(p->lch,x);
          else if(x>p->val)p->lch=remove(p->rch,x);
          //以下为找到了之后
          else if(p->lch==NULL)//情况1
          {
              node *q=p->rch;
              delete p;
              return q;
          }
          else if(p->lch->rch)//情况2
          {
              node *q=p->lch;
              q->rch=p->rch;
              delete p;
              return q;
          }
          else
          {
              node *q;
              for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个
              node *r=q->rch;//最大节点
              q->rch=r->lch;//最大节点左孩子提到最大节点位置
              r->lch=p->lch;//调整黑点左孩子为B
              r->rch=p->rch;//调整黑点右孩子为c
              delete p;//删除
              return r;//返回给父
          }
          return p;
     }

    Abstract Self-Balancing Binary Search Tree

     

    二叉搜索树

     

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
    具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/81741034

    我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,

    此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度

     

    概念引入

     

    Abstract Self-Balancing Binary Search Tree:自平衡二叉搜索树

    顾名思义:它在面对任意节点插入和删除时自动保持其高度

    常用算法有红黑树、AVL、Treap、伸展树、SB树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在O(log(n)),大大降低了操作的时间复杂度。这些结构为可变有序列表提供了有效的实现,并且可以用于其他抽象数据结构,例如关联数组优先级队列集合

    对于这些结构,他们都有自己的平衡性,比如:

    AVL树

    具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

    根据定义可知,这是根据深度最严苛的标准了,左右子树高度不能差的超过1.

    具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/85047648

     

    红黑树

    特性:
    (1)每个节点或者是黑色,或者是红色。
    (2)根节点是黑色。
    (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
    (4)如果一个节点是红色的,则它的子节点必须是黑色的。
    (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

    根据定义,确保没有一条路径会比其他路径长出2倍。

     

    size balance tree

    Size Balanced Tree(简称SBT)是一自平衡二叉查找树,是在计算机科学中用到的一种数据结构。它是由中国广东中山纪念中学的陈启峰发明的。陈启峰于2006年底完成论文《Size Balanced Tree》,并在2007年的全国青少年信息学奥林匹克竞赛冬令营中发表。由于SBT的拼写很容易找到中文谐音,它常被中国的信息学竞赛选手和ACM/ICPC选手们戏称为“傻B树”、“Super BT”等。相比红黑树、AVL树等自平衡二叉查找树,SBT更易于实现。据陈启峰在论文中称,SBT是“目前为止速度最快的高级二叉搜索树”。SBT能在O(log n)的时间内完成所有二叉搜索树(BST)的相关操作,而与普通二叉搜索树相比,SBT仅仅加入了简洁的核心操作Maintain。由于SBT赖以保持平衡的是size域而不是其他“无用”的域,它可以很方便地实现动态顺序统计中的selectrank操作。

    对于SBT的每一个结点 t,有如下性质:
       性质(a) s[ right[t] ]≥s[ left [ left[ t ] ] ], s[ right [ left[t] ] ]
       性质(b) s[ left[t] ]≥s[right[ right[t] ] ], s[ left[ right[t] ] ]
    即.每棵子树的大小不小于其兄弟的子树大小。

     

    伸展树

    伸展树(Splay Tree)是一种二叉排序树,它能在O(log n)内完成插入、查找和删除操作。它由Daniel Sleator和Robert Tarjan创造。它的优势在于不需要记录用于平衡树的冗余信息。在伸展树上的一般操作都基于伸展操作。

     

    Treap

    Treap是一棵二叉排序树,它的左子树和右子树分别是一个Treap,和一般的二叉排序树不同的是,Treap纪录一个额外的数据,就是优先级。Treap在以关键码构成二叉排序树的同时,还满足的性质(在这里我们假设节点的优先级大于该节点的孩子的优先级)。但是这里要注意的是Treap二叉堆有一点不同,就是二叉堆必须是完全二叉树,而Treap并不一定是。

     

     

     

     

    对比可以发现,AVL树对平衡性的要求比较严苛,每插入一个节点就很大概率面临调整。

    而红黑树对平衡性的要求没有那么严苛。可能是多次插入攒够了一下调整。。。

     

    把每一个树的细节都扣清楚是一件挺无聊的事。。虽然据说红黑树都成了面试必问内容,但是实在是不想深究那些细节,这些树的基本操作也无非是那么两种:左旋,右旋。这些树的所有操作和情况,都是这两种动作的组合罢了。

    所以本文先介绍这两种基本操作,等以后有时间(可能到找工作时),再把红黑树等结构的细节补上。

     

    最简单的旋转

     

    最简单的例子:

    这棵树,左子树深度为2,右子树深度为0,所以,根据AVL树或者红黑树的标准,它都不平衡。。

    那怎么办?转过来:

    是不是就平衡了?

    这就是我们的顺时针旋转,又叫,右旋,因为是以2为轴,把1转下来了。

    左旋同理。

     

    带子树旋转

    问题是,真正转起来可没有这么简单:

    这才是一颗搜索树的样子啊

    ABCD都代表是一颗子树。我们这三个点转了可不能不管这些子树啊对不对。

    好,我们想想这些子树怎么办。

    首先,AB子树没有关系,放在原地即可。

    D作为3的右子树,也可以不动,那剩下一个位置,会不会就是放C子树呢?

    我们想想能否这样做。

    原来:

    1)C作为2的右子树,内任何元素都比2大。

    2)C作为3左子树的一部分,内任何元素都比3小。

    转之后:

    1)C作为2的右子树的一部分,内任何元素都比2大。

    2)C作为3左子树,内任何元素都比3小。

    所以,C子树可以作为3的左子树,没有问题。

    这样,我们的操作就介绍完了。

    这种基本的变换达到了看似把树变的平衡的效果。

    左右旋转类似

     

    代码实现

    对于Abstract BinarySearchTree类,上面网址已经给出了思路和c++代码实现,把java再贴出来也挺无趣的,所以希望大家能自己实现。

    抽象自平衡二叉搜索树(AbstractSelfBalancingBinarySearchTree)的所有操作都是建立在二叉搜索树(BinarySearchTree )操作的基础上来进行的。

    各种自平衡二叉搜索树(AVL、红黑树等)的操作也是由Abstract自平衡二叉搜索树的基本操作:左旋、右旋构成。这个文章只写了左旋右旋基本操作,供以后各种selfBalancingBinarySearchTree使用。

    public abstract class AbstractSelfBalancingBinarySearchTree extends AbstractBinarySearchTree {
        protected Node rotateRight(Node node) {
            Node temp = node.left;//节点2
            temp.parent = node.parent;
            //节点3的父(旋转后节点2的父)
            node.left = temp.right;
            //节点3接收节点2的右子树
            if (node.left != null) {
                node.left.parent = node;
            }
    
            temp.right = node;
            //节点3变为节点2的右孩子
            node.parent = temp;
    
            //原来节点3的父(若存在),孩子变为节点2
            if (temp.parent != null) {
                if (node == temp.parent.left) {
                    temp.parent.left = temp;
                } else {
                    temp.parent.right = temp;
                }
            } else {
                root = temp;
            }
            return temp;
        }
    
        protected Node rotateLeft(Node node) {
            Node temp = node.right;
            temp.parent = node.parent;
            node.right = temp.left;
            if (node.right != null) {
                node.right.parent = node;
            }
            temp.left = node;
            node.parent = temp;
            if (temp.parent != null) {
                if (node == temp.parent.left) {
                    temp.parent.left = temp;
                } else {
                    temp.parent.right = temp;
                }
            } else {
                root = temp;
            }
            
            return temp;
        }
    }

     

    AVL Tree

     

    前言

     

    希望读者

    了解二叉搜索树

    了解左旋右旋基本操作

    https://blog.csdn.net/hebtu666/article/details/84992363

    直观感受直接到文章底部,有正确的调整策略动画,自行操作。

    二叉搜索树
     

    二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
    具体介绍和实现:https://blog.csdn.net/hebtu666/article/details/81741034

    我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度(O(log2n))同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,

    此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。
     

    AVL Tree

    计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis,他们在1962年的论文《An algorithm for the organization of information》中发表了它。

    这种结构是对平衡性要求最严苛的self-Balancing Binary Search Tree。

    旋转操作继承自self-Balancing Binary Search Tree

    public class AVLTree extends AbstractSelfBalancingBinarySearchTree

    旋转

    上面网址中已经介绍了二叉搜索树的调整和自平衡二叉搜索树的基本操作(左旋右旋),上篇文章我是这样定义左旋的:

    达到了   看似   更平衡的效果。

    我们回忆一下:

    看起来好像不是很平,对吗?我们转一下:

    看起来平了很多。

    但!是!

    只是看起来而已。

    我们知道。ABCD其实都是子树,他们也有自己的深度,如果是这种情况:

    我们简化一下:

    转之后(A上来,3作为A的右孩子,A的右子树作为新的3的左孩子):

    没错,旋转确实让树变平衡了,这是因为,不平衡是由A的左子树造成的,A的左子树深度更深。

    我们这样旋转实际上是让

    A的左子树相对于B提上去了两层,深度相对于B,-2,

    A的右子树相对于B提上去了一层,深度相对于B,-1.

    而如果是这样的:

    旋转以后:

    依旧是不平的。

    那我们怎么解决这个问题呢?

    先3的左子树旋转:

    细节问题:不再讲解

    这样,我们的最深处又成了左子树的左子树。然后再按原来旋转就好了。

     

    旋转总结

     

    那我们来总结一下旋转策略:

    单向右旋平衡处理LL:

    由于在*a的左子树根结点的左子树上插入结点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行一次右旋转操作;

    单向左旋平衡处理RR:

    由于在*a的右子树根结点的右子树上插入结点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行一次左旋转操作;

    双向旋转(先左后右)平衡处理LR:

    由于在*a的左子树根结点的右子树上插入结点,*a的平衡因子由1增至2,致使以*a为根的子树失去平衡,则需进行两次旋转(先左旋后右旋)操作。

    双向旋转(先右后左)平衡处理RL:

    由于在*a的右子树根结点的左子树上插入结点,*a的平衡因子由-1变为-2,致使以*a为根的子树失去平衡,则需进行两次旋转(先右旋后左旋)操作。

     

    深度的记录

     

    我们解决了调整问题,但是我们怎么发现树不平衡呢?总不能没插入删除一次都遍历一下求深度吧。

    当然要记录一下了。

    我们需要知道左子树深度和右子树深度。这样,我们可以添加两个变量,记录左右子树的深度。

    但其实不需要,只要记录自己的深度即可。然后左右子树深度就去左右孩子去寻找即可。

    这样就引出了一个问题:深度的修改、更新策略是什么呢?

    单个节点的深度更新

    本棵树的深度=(左子树深度,右子树深度)+1

    所以写出节点node的深度更新方法:

        private static final void updateHeight(AVLNode node) {
    //不存在孩子,为-1,最后+1,深度为0
            int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;
            int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;
            node.height = 1 + Math.max(leftHeight, rightHeight);
        }

     

    写出旋转代码

    配合上面的方法和文章头部给出文章Abstract Self-Balancing Binary Search Tree的旋转,我们可以AVL树的四种旋转:

        private Node avlRotateLeft(Node node) {
            Node temp = super.rotateLeft(node);
            updateHeight((AVLNode)temp.left);
            updateHeight((AVLNode)temp);
            return temp;
        }
    
        private Node avlRotateRight(Node node) {
            Node temp = super.rotateRight(node);
            updateHeight((AVLNode)temp.right);
            updateHeight((AVLNode)temp);
            return temp;
        }
    
        protected Node doubleRotateRightLeft(Node node) {
            node.right = avlRotateRight(node.right);
            return avlRotateLeft(node);
        }
    
        protected Node doubleRotateLeftRight(Node node) {
            node.left = avlRotateLeft(node.left);
            return avlRotateRight(node);
        }

    请自行模拟哪些节点的深度记录需要修改。

     

    总写调整方法

     

    我们写出了旋转的操作和相应的深度更新。

    现在我们把这些方法分情况总写。

        private void rebalance(AVLNode node) {
            while (node != null) {
                Node parent = node.parent;
                
                int leftHeight = (node.left == null) ? -1 : ((AVLNode) node.left).height;
                int rightHeight = (node.right == null) ? -1 : ((AVLNode) node.right).height;
                int nodeBalance = rightHeight - leftHeight;
    
                if (nodeBalance == 2) {
                    if (((AVLNode)node.right.right).height+1 == rightHeight) {
                        node = (AVLNode)avlRotateLeft(node);
                        break;
                    } else {
                        node = (AVLNode)doubleRotateRightLeft(node);
                        break;
                    }
                } else if (nodeBalance == -2) {
                    if (((AVLNode)node.left.left).height+1 == leftHeight) {
                        node = (AVLNode)avlRotateRight(node);
                        break;
                    } else {
                        node = (AVLNode)doubleRotateLeftRight(node);
                        break;
                    }
                } else {
                    updateHeight(node);//平衡就一直往上更新高度
                }
                
                node = (AVLNode)parent;
            }
        }

    插入完工

     

    我们的插入就完工了。

        public Node insert(int element) {
            Node newNode = super.insert(element);//插入
            rebalance((AVLNode)newNode);//调整
            return newNode;
        }

     

    删除

    也是一样的思路,自底向上,先一路修改高度后,进行rebalance调整。

        public Node delete(int element) {
            Node deleteNode = super.search(element);
            if (deleteNode != null) {
                Node successorNode = super.delete(deleteNode);
                //结合上面网址二叉搜索树实现的情况介绍
                if (successorNode != null) {
                    // if replaced from getMinimum(deleteNode.right) 
                    // then come back there and update heights
                    AVLNode minimum = successorNode.right != null ? (AVLNode)getMinimum(successorNode.right) : (AVLNode)successorNode;
                    recomputeHeight(minimum);
                    rebalance((AVLNode)minimum);
                } else {
                    recomputeHeight((AVLNode)deleteNode.parent);//先修改
                    rebalance((AVLNode)deleteNode.parent);//再调整
                }
                return successorNode;
            }
            return null;
        }
        /**
         * Recomputes height information from the node and up for all of parents. It needs to be done after delete.
         */
        private void recomputeHeight(AVLNode node) {
           while (node != null) {
              node.height = maxHeight((AVLNode)node.left, (AVLNode)node.right) + 1;
              node = (AVLNode)node.parent;
           }
        }
        
        /**
         * Returns higher height of 2 nodes. 
         */
        private int maxHeight(AVLNode node1, AVLNode node2) {
            if (node1 != null && node2 != null) {
                return node1.height > node2.height ? node1.height : node2.height;
            } else if (node1 == null) {
                return node2 != null ? node2.height : -1;
            } else if (node2 == null) {
                return node1 != null ? node1.height : -1;
            }
            return -1;
        }

    请手动模拟哪里的高度需要改,哪里不需要改。

     

    直观表现程序

     

    如果看的比较晕,或者直接从头跳下来的同学,这个程序是正确的模拟了,维护AVL树的策略和一些我没写的基本操作。大家可以自己操作,直观感受一下。

    https://www.cs.usfca.edu/~galles/visualization/AVLtree.html?utm_source=qq&utm_medium=social&utm_oi=826801573962338304

     

    跳表介绍和实现

     

    想慢慢的给大家自然的引入跳表。

     

    想想,我们

    1)在有序数列里搜索一个数

    2)或者把一个数插入到正确的位置

    都怎么做?

    很简单吧

    对于第一个操作,我们可以一个一个比较,在数组中我们可以二分,这样比链表快

    对于第二个操作,二分也没什么用,因为找到位置还要在数组中一个一个挪位置,时间复杂度依旧是o(n)。

    那我们怎么发明一个查找插入都比较快的结构呢?

     

     

     

    可以打一些标记:

    这样我们把标记连起来,搜索一个数时先从标记开始搜起下一个标记比本身大的话就往下走,因为再往前就肯定不符合要求了。

    比如我们要搜索18:

    因为一次可以跨越好多数呀,自然快了一些。

    既然可以打标记,我们可以改进一下,选出一些数来再打一层标记:

    这样我们搜索20是这样的:

    最终我们可以打好多层标记,我们从最高层开始搜索,一次可以跳过大量的数(依旧是右边大了就往下走)。

    比如搜索26:

    最好的情况,就是每一层的标记都减少一半,这样到了顶层往下搜索,其实和二分就没什么两样,我们最底层用链表串起来,插入一个元素也不需要移动元素,所谓跳表就完成了一大半了。

     

    现在的问题是,我们对于一个新数,到底应该给它打几层标记呢?

    (刚开始一个数都没有,所以解决了这个问题,我们一直用这个策略更新即可)

    答案是。。。。。投硬币,全看脸。

    我其实有点惊讶,我以为会有某些很强的和数学相关的算法,可以保证一个很好的搜索效率,是我想多了。

    我们对于一个新数字,有一半概率可以打一层标记,有一半概率不可以打。

    对于打了一层标记的数,我们依旧是这个方法,它有一半概率再向上打一层标记,依次循环。

    所以每一层能到达的概率都少一半。

    各层的节点数量竟然就可以比较好的维护在很好的效率上(最完美的就是达到了二分的效果)

     

    再分析一下,其实对于同一个数字:

    等等。。

    其实没必要全都用指针,因为我们知道,通过指针找到一个数可比下标慢多了。

    所以同一个数字的所有标记,没必要再用指针,效率低还不好维护,用一个list保存即可。

    这样,我们就设计出来一个数字的所有标记组成的结构:

    	public static class SkipListNode {
    		public Integer value;//本身的值
    		public ArrayList<SkipListNode> nextNodes;
    //指向下一个元素的结点组成的数组,长度全看脸。
    
    		public SkipListNode(Integer value) {
    			this.value = value;
    			nextNodes = new ArrayList<SkipListNode>();
    		}
    	}

    将integer比较的操作封装一下:

    		private boolean lessThan(Integer a, Integer b) {
    			return a.compareTo(b) < 0;
    		}
    
    		private boolean equalTo(Integer a, Integer b) {
    			return a.compareTo(b) == 0;
    		}

    找到在本层应该往下拐的结点:

    		// Returns the node at a given level with highest value less than e
    		private SkipListNode findNext(Integer e, SkipListNode current, int level) {
    			SkipListNode next = current.nextNodes.get(level);
    			while (next != null) {
    				Integer value = next.value;
    				if (lessThan(e, value)) { // e < value
    					break;
    				}
    				current = next;
    				next = current.nextNodes.get(level);
    			}
    			return current;
    		}

    这样我们就写一个一层层往下找的方法,并且封装成find(Integer e)的形式:

    		// Returns the skiplist node with greatest value <= e
    		private SkipListNode find(Integer e) {
    			return find(e, head, maxLevel);
    		}
    
    		// Returns the skiplist node with greatest value <= e
    		// Starts at node start and level
    		private SkipListNode find(Integer e, SkipListNode current, int level) {
    			do {
    				current = findNext(e, current, level);
    			} while (level-- > 0);
    			return current;
    		}

    刚才的方法是找到最大的小于等于目标的值,如果找到的值等于目标,跳表中就存在这个目标。否则不存在。

    		public boolean contains(Integer value) {
    			SkipListNode node = find(value);
    			return node != null && node.value != null && equalTo(node.value, value);
    		}

    我们现在可以实现加入一个新点了,要注意把每层的标记打好:

    		public void add(Integer newValue) {
    			if (!contains(newValue)) {
    				size++;
    				int level = 0;
    				while (Math.random() < PROBABILITY) {
    					level++;//能有几层全看脸
    				}
    				while (level > maxLevel) {//大于当前最大层数
    					head.nextNodes.add(null);//直接连系统最大
    					maxLevel++;
    				}
    				SkipListNode newNode = new SkipListNode(newValue);
    				SkipListNode current = head;//前一个结点,也就是说目标应插current之后
    				do {//每一层往下走之前就可以设置这一层的标记了,就是链表插入一个新节点
    					current = findNext(newValue, current, level);
    					newNode.nextNodes.add(0, current.nextNodes.get(level));
    					current.nextNodes.set(level, newNode);
    				} while (level-- > 0);
    			}
    		}

    删除也是一样的

    		public void delete(Integer deleteValue) {
    			if (contains(deleteValue)) {
    				SkipListNode deleteNode = find(deleteValue);
    				size--;
    				int level = maxLevel;
    				SkipListNode current = head;
    				do {//就是一个链表删除节点的操作
    					current = findNext(deleteNode.value, current, level);
    					if (deleteNode.nextNodes.size() > level) {
    						current.nextNodes.set(level, deleteNode.nextNodes.get(level));
    					}
    				} while (level-- > 0);
    			}
    		}

    作为一个容器,Iterator那是必须有的吧,里面肯定有hasNext和next吧?

    	public static class SkipListIterator implements Iterator<Integer> {
    		SkipList list;
    		SkipListNode current;
    
    		public SkipListIterator(SkipList list) {
    			this.list = list;
    			this.current = list.getHead();
    		}
    
    		public boolean hasNext() {
    			return current.nextNodes.get(0) != null;
    		}
    
    		public Integer next() {
    			current = current.nextNodes.get(0);
    			return current.value;
    		}
    	}

    这个跳表我们就实现完了。

    现实工作中呢,我们一般不会让它到无限多层,万一有一个数它人气爆炸随机数冲到了一万层呢?

    所以包括redis在内的一些跳表实现,都是规定了一个最大层数的。

    别的好像也没什么了。

    最后贴出所有代码。

    import java.util.ArrayList;
    import java.util.Iterator;
    
    public SkipListDemo {
    
    	public static class SkipListNode {
    		public Integer value;
    		public ArrayList<SkipListNode> nextNodes;
    
    		public SkipListNode(Integer value) {
    			this.value = value;
    			nextNodes = new ArrayList<SkipListNode>();
    		}
    	}
    
    	public static class SkipListIterator implements Iterator<Integer> {
    		SkipList list;
    		SkipListNode current;
    
    		public SkipListIterator(SkipList list) {
    			this.list = list;
    			this.current = list.getHead();
    		}
    
    		public boolean hasNext() {
    			return current.nextNodes.get(0) != null;
    		}
    
    		public Integer next() {
    			current = current.nextNodes.get(0);
    			return current.value;
    		}
    	}
    
    	public static class SkipList {
    		private SkipListNode head;
    		private int maxLevel;
    		private int size;
    		private static final double PROBABILITY = 0.5;
    
    		public SkipList() {
    			size = 0;
    			maxLevel = 0;
    			head = new SkipListNode(null);
    			head.nextNodes.add(null);
    		}
    
    		public SkipListNode getHead() {
    			return head;
    		}
    
    		public void add(Integer newValue) {
    			if (!contains(newValue)) {
    				size++;
    				int level = 0;
    				while (Math.random() < PROBABILITY) {
    					level++;
    				}
    				while (level > maxLevel) {
    					head.nextNodes.add(null);
    					maxLevel++;
    				}
    				SkipListNode newNode = new SkipListNode(newValue);
    				SkipListNode current = head;
    				do {
    					current = findNext(newValue, current, level);
    					newNode.nextNodes.add(0, current.nextNodes.get(level));
    					current.nextNodes.set(level, newNode);
    				} while (level-- > 0);
    			}
    		}
    
    		public void delete(Integer deleteValue) {
    			if (contains(deleteValue)) {
    				SkipListNode deleteNode = find(deleteValue);
    				size--;
    				int level = maxLevel;
    				SkipListNode current = head;
    				do {
    					current = findNext(deleteNode.value, current, level);
    					if (deleteNode.nextNodes.size() > level) {
    						current.nextNodes.set(level, deleteNode.nextNodes.get(level));
    					}
    				} while (level-- > 0);
    			}
    		}
    
    		// Returns the skiplist node with greatest value <= e
    		private SkipListNode find(Integer e) {
    			return find(e, head, maxLevel);
    		}
    
    		// Returns the skiplist node with greatest value <= e
    		// Starts at node start and level
    		private SkipListNode find(Integer e, SkipListNode current, int level) {
    			do {
    				current = findNext(e, current, level);
    			} while (level-- > 0);
    			return current;
    		}
    
    		// Returns the node at a given level with highest value less than e
    		private SkipListNode findNext(Integer e, SkipListNode current, int level) {
    			SkipListNode next = current.nextNodes.get(level);
    			while (next != null) {
    				Integer value = next.value;
    				if (lessThan(e, value)) { // e < value
    					break;
    				}
    				current = next;
    				next = current.nextNodes.get(level);
    			}
    			return current;
    		}
    
    		public int size() {
    			return size;
    		}
    
    		public boolean contains(Integer value) {
    			SkipListNode node = find(value);
    			return node != null && node.value != null && equalTo(node.value, value);
    		}
    
    		public Iterator<Integer> iterator() {
    			return new SkipListIterator(this);
    		}
    
    		/******************************************************************************
    		 * Utility Functions *
    		 ******************************************************************************/
    
    		private boolean lessThan(Integer a, Integer b) {
    			return a.compareTo(b) < 0;
    		}
    
    		private boolean equalTo(Integer a, Integer b) {
    			return a.compareTo(b) == 0;
    		}
    
    	}
    
    	public static void main(String[] args) {
    
    	}
    
    }
    
    

    c语言实现排序和查找所有算法

     

     c语言版排序查找完成,带详细解释,一下看到爽,能直接运行看效果。

     

    /* Note:Your choice is C IDE */
    #include "stdio.h"
    #include"stdlib.h"
    #define MAX 10
    void SequenceSearch(int *fp,int Length);
    void Search(int *fp,int length);
    void Sort(int *fp,int length);
    /*
    注意:
        1、数组名x,*(x+i)就是x[i]哦
    
    */
    
    
    /*
    ================================================
    功能:选择排序
    输入:数组名称(数组首地址)、数组中元素个数
    ================================================
    */
    void select_sort(int *x, int n)
    {
        int i, j, min, t;
        for (i=0; i<n-1; i++) /*要选择的次数:下标:0~n-2,共n-1次*/
        {
            min = i; /*假设当前下标为i的数最小,比较后再调整*/
            for (j=i+1; j<n; j++)/*循环找出最小的数的下标是哪个*/
            {
                if (*(x+j) < *(x+min))
                    min = j; /*如果后面的数比前面的小,则记下它的下标*/
            }
            if (min != i) /*如果min在循环中改变了,就需要交换数据*/
            {
                t = *(x+i);
                *(x+i) = *(x+min);
                *(x+min) = t;
            }
        }
    }
    /*
    ================================================
    功能:直接插入排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    ================================================
    */
    
    void insert_sort(int *x, int n)
    {
        int i, j, t;
        for (i=1; i<n; i++) /*要选择的次数:下标1~n-1,共n-1次*/
        {
            /*
             暂存下标为i的数。注意:下标从1开始,原因就是开始时
             第一个数即下标为0的数,前面没有任何数,认为它是排
             好顺序的。
            */
            t=*(x+i);
            for (j=i-1; j>=0 && t<*(x+j); j--) /*注意:j=i-1,j--,这里就是下标为i的数,在它前面有序列中找插入位置。*/
            {
                *(x+j+1) = *(x+j); /*如果满足条件就往后挪。最坏的情况就是t比下标为0的数都小,它要放在最前面,j==-1,退出循环*/
            }
            *(x+j+1) = t; /*找到下标为i的数的放置位置*/
        }
    }
    /*
    ================================================
    功能:冒泡排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    ================================================
    */
    void bubble_sort0(int *x, int n)
    {
        int j, h, t;
        for (h=0; h<n-1; h++)/*循环n-1次*/
        {
            for (j=0; j<n-2-h; j++)/*每次做的操作类似*/
            {
                if (*(x+j) > *(x+j+1)) /*大的放在后面,小的放到前面*/
                {
                    t = *(x+j);
                    *(x+j) = *(x+j+1);
                    *(x+j+1) = t; /*完成交换*/
                }
            }
        }
    }
    /*优化:记录最后下沉位置,之后的肯定有序*/
    void bubble_sort(int *x, int n)
    {
        int j, k, h, t;
        for (h=n-1; h>0; h=k) /*循环到没有比较范围*/
        {
            for (j=0, k=0; j<h; j++) /*每次预置k=0,循环扫描后更新k*/
            {
                if (*(x+j) > *(x+j+1)) /*大的放在后面,小的放到前面*/
                {
                    t = *(x+j);
                    *(x+j) = *(x+j+1);
                    *(x+j+1) = t; /*完成交换*/
                    k = j; /*保存最后下沉的位置。这样k后面的都是排序排好了的。*/
                }
            }
        }
    }
    /*
    ================================================
    功能:希尔排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    ================================================
    */
    
    void shell_sort(int *x, int n)
    {
        int h, j, k, t;
        for (h=n/2; h>0; h=h/2) /*控制增量*/
        {
            for (j=h; j<n; j++) /*这个实际上就是上面的直接插入排序*/
            {
                t = *(x+j);
                for (k=j-h; (k>=0 && t<*(x+k)); k-=h)
                {
                    *(x+k+h) = *(x+k);
                }
                *(x+k+h) = t;
            }
        }
    }
    /*
    ================================================
    功能:快速排序
    输入:数组名称(也就是数组首地址)、数组中起止元素的下标
    注:自己画画
    ================================================
    */
    
    void quick_sort(int *x, int low, int high)
    {
        int i, j, t;
        if (low < high) /*要排序的元素起止下标,保证小的放在左边,大的放在右边。这里以下标为low的元素(最左边)为基准点*/
        {
            i = low;
            j = high;
            t = *(x+low); /*暂存基准点的数*/
            while (i<j) /*循环扫描*/
            {
                while (i<j && *(x+j)>t) /*在右边的只要比基准点大仍放在右边*/
                {
                    j--; /*前移一个位置*/
                }
                if (i<j)
                {
                    *(x+i) = *(x+j); /*上面的循环退出:即出现比基准点小的数,替换基准点的数*/
                    i++; /*后移一个位置,并以此为基准点*/
                }
                while (i<j && *(x+i)<=t) /*在左边的只要小于等于基准点仍放在左边*/
                {
                    i++; /*后移一个位置*/
                }
                if (i<j)
                {
                    *(x+j) = *(x+i); /*上面的循环退出:即出现比基准点大的数,放到右边*/
                    j--; /*前移一个位置*/
                }
            }
            *(x+i) = t; /*一遍扫描完后,放到适当位置*/
            quick_sort(x,low,i-1);   /*对基准点左边的数再执行快速排序*/
            quick_sort(x,i+1,high);   /*对基准点右边的数再执行快速排序*/
        }
    }
    /*
    ================================================
    功能:堆排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    注:画画
    ================================================
    */
    /*
    功能:建堆
    输入:数组名称(也就是数组首地址)、参与建堆元素的个数、从第几个元素开始
    */
    void sift(int *x, int n, int s)
    {
        int t, k, j;
        t = *(x+s); /*暂存开始元素*/
        k = s;   /*开始元素下标*/
        j = 2*k + 1; /*左子树元素下标*/
        while (j<n)
        {
            if (j<n-1 && *(x+j) < *(x+j+1))/*判断是否存在右孩子,并且右孩子比左孩子大,成立,就把j换为右孩子*/
            {
                j++;
            }
            if (t<*(x+j)) /*调整*/
            {
                *(x+k) = *(x+j);
                k = j; /*调整后,开始元素也随之调整*/
                j = 2*k + 1;
            }
            else /*没有需要调整了,已经是个堆了,退出循环。*/
            {
                break;
            }
        }
        *(x+k) = t; /*开始元素放到它正确位置*/
    }
    /*
    功能:堆排序
    输入:数组名称(也就是数组首地址)、数组中元素个数
    注:
                *
             *     *
           *   -  *   *
          * * * 
    建堆时,从从后往前第一个非叶子节点开始调整,也就是“-”符号的位置
    */
    void heap_sort(int *x, int n)
    {
        int i, k, t;
    //int *p;
        for (i=n/2-1; i>=0; i--)
        {
            sift(x,n,i); /*初始建堆*/
        }
        for (k=n-1; k>=1; k--)
        {
            t = *(x+0); /*堆顶放到最后*/
            *(x+0) = *(x+k);
            *(x+k) = t;
            sift(x,k,0); /*剩下的数再建堆*/
        }
    }
    
    
    
    
    // 归并排序中的合并算法
    void Merge(int a[], int start, int mid, int end)
    {
        int i,k,j, temp1[10], temp2[10];
        int n1, n2;
        n1 = mid - start + 1;
        n2 = end - mid;
    
        // 拷贝前半部分数组
        for ( i = 0; i < n1; i++)
        {
            temp1[i] = a[start + i];
        }
        // 拷贝后半部分数组
        for (i = 0; i < n2; i++)
        {
            temp2[i] = a[mid + i + 1];
        }
        // 把后面的元素设置的很大
        temp1[n1] = temp2[n2] = 1000;
        // 合并temp1和temp2
        for ( k = start, i = 0, j = 0; k <= end; k++)
        {
            //小的放到有顺序的数组里
            if (temp1[i] <= temp2[j])
            {
                a[k] = temp1[i];
                i++;
            }
            else
            {
                a[k] = temp2[j];
                j++;
            }
        }
    }
    
    // 归并排序
    void MergeSort(int a[], int start, int end)
    {
        if (start < end)
        {
            int i;
            i = (end + start) / 2;
            // 对前半部分进行排序
            MergeSort(a, start, i);
            // 对后半部分进行排序
            MergeSort(a, i + 1, end);
            // 合并前后两部分
            Merge(a, start, i, end);
        }
    }
    /*顺序查找*/
    void SequenceSearch(int *fp,int Length)
    {
        int i;
        int data;
        printf("开始使用顺序查询.\n请输入你想要查找的数据.\n");
        scanf("%d",&data);
        for(i=0; i<Length; i++)
            if(fp[i]==data)
            {
                printf("经过%d次查找,查找到数据%d,表中位置为%d.\n",i+1,data,i);
                return ;
            }
        printf("经过%d次查找,未能查找到数据%d.\n",i,data);
    }
    /*二分查找*/
    void Search(int *fp,int Length)
    {
        int data;
        int bottom,top,middle;
        int i=0;
        printf("开始使用二分查询.\n请输入你想要查找的数据.\n");
        scanf("%d",&data);
        printf("由于二分查找法要求数据是有序的,现在开始为数组排序.\n");
        Sort(fp,Length);
        printf("数组现在已经是从小到大排列,下面将开始查找.\n");
        bottom=0;
        top=Length;
        while (bottom<=top)
        {
            middle=(bottom+top)/2;
            i++;
            if(fp[middle]<data)
            {
                bottom=middle+1;
            }
            else if(fp[middle]>data)
            {
                top=middle-1;
            }
            else
            {
                printf("经过%d次查找,查找到数据%d,在排序后的表中的位置为%d.\n",i,data,middle);
                return;
            }
        }
        printf("经过%d次查找,未能查找到数据%d.\n",i,data);
    }
    
    /*
    
    下面测试了
    
    */
    void Sort(int *fp,int Length)
    {
        int temp;
        int i,j,k;
        printf("现在开始为数组排序,排列结果将是从小到大.\n");
        for(i=0; i<Length; i++)
            for(j=0; j<Length-i-1; j++)
                if(fp[j]>fp[j+1])
                {
                    temp=fp[j];
                    fp[j]=fp[j+1];
                    fp[j+1]=temp;
                }
        printf("排序完成!\n下面输出排序后的数组:\n");
        for(k=0; k<Length; k++)
        {
            printf("%5d",fp[k]);
        }
        printf("\n");
    
    }
    /*构造随机输出函数类*/
    void input(int a[])
    {
        int i;
        srand( (unsigned int)time(NULL) );
        for (i = 0; i < 10; i++)
        {
            a[i] = rand() % 100;
        }
        printf("\n");
    }
    /*构造键盘输入函数类*/
    /*void input(int *p)
    {
         int i;
         printf("请输入 %d 个数据 :\n",MAX);
          for (i=0; i<MAX; i++)
          {
           scanf("%d",p++);
          }
          printf("\n");
    }*/
    /*构造输出函数类*/
    void output(int *p)
    {
        int i;
        for ( i=0; i<MAX; i++)
        {
            printf("%d ",*p++);
        }
    }
    void main()
    {
        int start=0,end=3;
        int *p, i, a[MAX];
        int count=MAX;
        int arr[MAX];
        int choise=0;
        /*printf("请输入你的数据的个数:\n");
        scanf("%d",&count);*/
        /* printf("请输入%d个数据\n",count);
        for(i=0;i<count;i++)
        {
           scanf("%d",&arr[i]);
        }*/
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        do
        {
            printf("1.使用顺序查询.\n2.使用二分查找法查找.\n3.退出\n");
            scanf("%d",&choise);
            if(choise==1)
                SequenceSearch(a,count);
            else if(choise==2)
                Search(a,count);
            else if(choise==3)
                break;
        }
        while (choise==1||choise==2||choise==3);
    
    
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试选择排序*/
        p = a;
        printf("选择排序之后的数据:\n");
        select_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /**/
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试直接插入排序*/
        printf("直接插入排序之后的数据:\n");
        p = a;
        insert_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试冒泡排序*/
        printf("冒泡排序之后的数据:\n");
        p = a;
        insert_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试快速排序*/
        printf("快速排序之后的数据:\n");
        p = a;
        quick_sort(p,0,MAX-1);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试堆排序*/
        printf("堆排序之后的数据:\n");
        p = a;
        heap_sort(p,MAX);
        output(a);
        printf("\n");
        system("pause");
        /*录入测试数据*/
        input(a);
        printf("随机初始数组为:\n");
        output(a);
        printf("\n");
        /*测试归并排序*/
        printf("归并排序之后的数据:\n");
        p = a;
        MergeSort(a,start,end);
        output(a);
        printf("\n");
        system("pause");
    }

     

     

    展开全文
  • 甲鱼零基础入门学习python笔记

    万次阅读 多人点赞 2019-08-14 11:06:30
    甲鱼老师零基础入门学习Python全套资料百度云(包括甲鱼零基础入门学习Python全套视频+全套源码+全套PPT课件+全套课后题及Python常用工具包链接、电子书籍等)请往我的资源... 000 愉快的开始 ...
  • 文章目录1 软件测试流程2 软件测试过程模型2.1 V模型(重点)2.2 W模型(重点)2.3 H模型2.4 X模型3 软件测试过程理念 1 软件测试流程 软件测试流程独立于开发之外的流程。在执行测试时,与开发有交集。 牢记该流程...
  • 超硬核!数据结构学霸笔记,考试面试吹牛就靠它

    万次阅读 多人点赞 2021-03-26 11:11:21
    执行频度=算法中每一条语句执行次数的和 一般认定每条语句执行一次所需时间为单位时间(常数时间)O(1) 几个知识和问题: 1)循环执行次数n+1次,不是n次。第一次执行i=1和判断i以后执行n次判断和i++。所以该...
  • 从今天开始就来带领大家学习微信程序了,只要你跟着我一步步来,相信你也可以上线一款属于自己的微信程序 一,认识程序 微信⼩程序,简称⼩程序,英⽂名 Mini Program Mini Program ,是⼀种不需要下载安装...
  • STM32最小系统板介绍

    万次阅读 多人点赞 2021-07-04 17:21:57
    而且你直接电源接到图中5V这个引脚,旁边的3.3V引脚还能继续供出3.3V电流供别的使用 2+.上图中可以看到有三个G(GND),两个3.3V。其实这三个GND是相通的,两个3.3也是相通的,可以用万用表测量一下试试。 3+.接...
  • getUserInfo换成getUserProfile就可以了 <button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button> getUserProfile(e) { // 推荐使用wx.getUserProfile获取...
  • 1小时1篇文学会python再做个飞机大战游戏

    万次阅读 多人点赞 2021-04-30 16:02:07
    媛:因为 a 是一个变量名,用一个等于号就可以等于号右边的值存到左边的变量中,然后最开始你说在 python 中字符串是需要双引号标记,那就是这样写咯。 C:不错,理解透彻,那我们开始显示变量里面所存储的...
  • 本篇文章想要系统的传统汽车和新能源汽车的交流发电和V2H和V2G的功能梳理一下,全文主要分几个部分来分别介绍,在汽车使用中,一种能够将铅酸电池电压DC12V直流电转换(在新能源汽车里面就是用高压电源)为和市电...
  • 微服务[v1.0.0][服务调用]

    万次阅读 2020-07-15 14:38:25
    事件溯源(Event Sourcing) 这是一种具有颠覆性质的的设计,它系统中所有的数据都以事件(Event)的方式记录下来,它的持久存储Event Store, 一般是建立在数据库或消息队列(例如Kafka)基础之上,并提供了对...
  • 2020ChinaJoy全是姐姐,程序员小伙当场流鼻血

    万次阅读 多人点赞 2020-08-03 18:30:49
    看到这篇水文的时候你应该知道了,你们的敖丙这周末又偷懒没写技术文和拍视频,偷偷跑去上海浪了两天,不过技术文我工作日还是得熬夜搞一篇出来,女读者的面试视频剪辑进度10%了,这周应该可以结束。 再说你们以为敖...
  • 无刷电机foc笔记2(V/F控制实现)

    千次阅读 2020-01-31 19:52:53
    ctime:2019-09-06 18:42:18 +0800|1567766538 标签(空格分隔): 硬件 技术 VF控制实际上是一种开环的驱动方式。在前一篇博文中已经讲了,实际...那么通过改变电压与频率的比值,也就是V/F,就可以进行调速以及调...
  • HDFS文件问题解决方案与启发实践

    千次阅读 2017-01-08 14:25:07
    前言 继上文聊聊HDFS Block...在本文的阐述过程中,我们将通过一个平时遇到的典型问题-HDFS文件过多问题作为贯穿全文的一个核心要点。在下文中,笔者将会介绍文件的缘由,现有解决办法,新的解决方案等等内容。
  • 由于不依赖文档, 因此要时常和项目组的人沟通 测试人员自己的文档不再需要写测试用例, 而是需要画出思维导图, 将自己的测试点整理出来. 有些公司虽然使用的是敏捷开发模型, 但要求测试和研发人员要按照传统模型的...
  • stm32f103最小系统的组成及各部分功能

    万次阅读 多人点赞 2021-06-23 13:53:24
    一、什么最小系统 最小系统就像我们人,我们活着每天都要吃饭,喝水。 饭和水就是我们的”燃料”,离开它们我们就挂逼了。 其实在芯片的世界也一样,单片机想要正常运行,它也需要”燃料”,这些燃料的组成...
  • (小野坚持写好每一篇文章,喜欢就点个赞吧...2017年5月以太坊诞生短短三年多时间里,全球已经有200多个以太坊应用被开发出来。 以太坊可以用来编程,分散,担保和交易任何事物:投票,域名,金融交易所,众筹,公司...
  • 微信程序推送模板消息

    万次阅读 多人点赞 2018-07-24 18:08:34
    今天KingYiFan给大家分享一下程序模板消息推送: 根据微信程序最新机制:2019年10月份微信发布了最新模板消息整改公告,由模板消息更改为订阅消息,模板消息将于2020年1月10日下线,下线后这篇文章将失效。最新...
  • 程序流量主运营技巧

    千次阅读 2020-02-15 16:09:24
    原创来自...本文不涉及任何需要资质的程序(如:视频类目)。程序流量主是个人和小微企业主要变现途径之一,满1000人即可开通流量主(登录mp.weixin.qq.com,左侧边栏-推广-...
  • 最近开始自己琢磨开发微信程序,遇到些问题,现在做一下总结。 今天有遇到一个需要获取到列表中某一项值,并在其他地方调用的问题。其实思路和获取列表索引的道理一样。如果你还不知道如何获取列表的索引,那么...
  • 深度相机(五)--Kinect v2.0

    万次阅读 2016-09-25 22:20:07
    这里主要记录一下V1.0和V1.2的区别。原文:http://www.cnblogs.com/TracePlus/p/4136297.html 本文主要是对传感的配置和运行条件进行彻底的比较。  本连载介绍的Kinect for Windows Developer Preview是暂定的,...
  • 针对异步电机,为了保证电机磁通和出力不变 ,电机改变频率时,需维持电压V和频率F的比率近似不变,所以这种方式称为恒压频比(VF)控制。VF控制:控制简单,通用性强,经济性好,用于速度精度要求不十分严格或负载...
  • 决战Go语言从入门到入土v0.1

    万次阅读 多人点赞 2022-02-12 21:42:06
    下载地址:https://gitcode.net/as604049322/blog_pdf ... Go 1.13 版本 Go 语言项目自身的 go.mod 文件内容: module std go 1.13 require ( golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 golang.org/x/...
  • 牛逼!Java 从入门到精通,超全汇总版

    万次阅读 多人点赞 2021-05-06 19:40:33
    二、面向对象 为了大象装进冰箱,需要做三个动作(或者行为)。每个动作有一个执行者,它就是对象。 思路: 1、冰箱,你给我把门打开。 2、冰箱,你给我大象装进去(或者说,大象,你给我钻到冰箱里去)。 3...
  • 这个水平的话,改点bug或许没问题,但如果将来Java加了新的语言特性,能知道如何在JVM中实现吗? 一句话,我在研究ART时深刻感受到了更多自己不懂的东西。显然,一门语言除了语言自己外还包括其它一些至关重要的...
  • 但也有些遗憾,大学的时候没有这些计算机基础课程学好,有些甚至没有学,导致工作后有很长一段时间蛮吃力的,全靠近些年“废寝忘食”的补课,才有所好转。 希望学弟学妹们,能从我这些经验中获得一些启发,少走...
  • 通俗理解kaggle比赛大杀器xgboost

    万次阅读 多人点赞 2018-08-04 14:18:38
    因为xgboost本质上还是一个GBDT,但是力争速度和效率发挥到极致,所以X (Extreme) GBoosted。包括前面说过,两者都是boosting方法。 GBDT的原理很简单,就是所有弱分类器的结果相加等于预测值,然后下一个弱...
  • 图论(一)--基础概念

    万次阅读 2019-01-03 12:02:40
    §顶点&amp;amp;...欧拉于1736年研究并解决了此问题,他问题归结为如右图的“一笔画”问题,证明上述走法是不可能的。 图论基础知识——顶点 通过上文的故事版和正经版的对比,可以很容易...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 123,235
精华内容 49,294
关键字:

怎么把小v叫出来