数据结构初阶第四节.链表详讲

Posted 未央.303

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构初阶第四节.链表详讲相关的知识,希望对你有一定的参考价值。

文章目录

前言

一、单链表的概念

二、链表的创建

2.1链表的初始化

2.2 打印链表

2.3 获取链表的长度:

2.4 判断链表是否为空:

三、新增结点

        3.1头插:

        3.2 指定下标插入

四、删除结点:

        4.1 头删

        4.2 指定下标的删除

        4.3 删除链表中的指定元素

五、单链表查找:

六、附录

总代码

测试代码:

总结


前言

前一小节内容,我们学习了有关线性表中顺序表的有关知识;今天我们将学习有关单链表的有关知识;单链表也是数据结构中的一大重点;今天就让我们来学习它吧!!!!!!!!!!


一、单链表的概念

链表是一种物理存储结构上非连续、非顺序的存储结构,整个链表就是通过对各个结点地址的链式储存来实现的 。(链表就是由一个一个结点所组成的)

举例说明:

链表的结构有点类似于火车,火车的每一节车厢都由插销,和钩子链接起来;

那么怎么这一个一个的结点是怎样链接起来呢?在Java中他是通过引用所指向的地址来链接起来的。

什么意思呢?就是说链表的每一个结点元素都分为连两部分,一部分用来储存数值,另一部分来储存地址。


储存的是谁的地址呀!就是下一个结点的地址

比如说在链表中有三个结点,那么他们之间的关系是这样的:


既然说了,链表是由一个一个的结点组成的,那么在Java中结点是怎样定义的呢?

我们从上面也可以发现在每一个结点都是一个独立的小个体,我们不妨把他抽象为一个内部类,并放到单链表这个类的里面。这样我们就可以在单链表这个类里面使用我们结点这个小个体,并尝试把它串起来。

注意:本篇文章所讨论的单链表用到了两个文件:

  1. 单链表的构建文件MyLinkList.java
  2. 单链表的测试文件hLinkListTest.java

二、链表的创建

代码:

public class MyLinkList 
    ListNode head = null; // 声明链表中的头结点
 
    //创建单链表结构,将链表的每一个结点都定义成一个内部类
    public class ListNode 
        public int val;        // 该结点的数值域,储存该结点的数值
        public ListNode next; // 该结点的next域,储存的是下一个结点的地址,两个结点间正是通过next域产生关联
 
        public ListNode(int val)  // 构造方法,给新生成的结点赋值,同时next默认为null
            this.val = val;
        
    
 

如图所示:我们定义了一个MyLinkList(单链表)类,同时在该类中还定义了一个内部类(结点类)。这样就创建了一个基本的链表结构。


 

但光这样肯定是不行的,我们对链表有一些基本的操作方法如下:

// 链表初始化
    public ListNode listInit()  
 
    // 打印链表
    public void linkedListPrint()  
    // 获取链表长度
    public int getSize()  return -1; 
 
    //判断该链表是否为空
    private void isEmpty()  

2.1链表的初始化

首先我们要做的就是对我们的链表进行初始化的操作,那么怎么初始化呢?

当然是创建一些结点,并在每一个结点中都把下一个结点的地址给储存起来,然后相互链接起来的呀

代码示例:

// 链表初始化
    public ListNode listInit() 
        Scanner in = new Scanner(System.in);
        System.out.print("请输入你要构建的链表的初始长度:");
        int n = in.nextInt();
        System.out.print("请输入链表第1个元素的值:");
        int firstVal = in.nextInt();  //
        this.head = new ListNode(firstVal); // 创建链表的第一个结点,将我们链表的头结点引用head指向链表的第一个结点
 
        ListNode cur = head; // 创建一个引用cur去完成链表的初始化(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        for (int i = 1; i < n; i++) 
            System.out.print("请输入链表第" + (i + 1) + "个元素的值:");
            int val = in.nextInt();
            ListNode node = new ListNode(val);
            // 当前结点的next域存放的是对下一个结点的引用变量node,node储存的就是下一个结点的地址
            cur.next = node;  // 当前结点的next域存放的是对下一个结点的引用变量node
            cur = node;       // 将当前结点移向下一个结点
        
        return this.head; // 返回该链表的头结点
    

解析:

如图所示:

对于结点的链接:用的就是cur这个结点引用变量;

当我们的cur引用也指向第一个结点后,cur.next代表的就是第一个结点的next域,只要next域里储存了下一个结点的地址,这两个结点就链接起来了。 

那要是再来个第三个结点node呢?  

不要着急,我们只需要让cur = cur.next,cur.next = node就完成了结点间的链接;

当cur = cur.next 后,引用变量cur现在所引用的就变成了第二个结点,那么cur.next = node的作用就是将第三个结点的地址储存到了第二个结点的next域里

接下第四、第五个结点也大致是这样的操作; 

你可以会问:为啥非得要再定义一个结点的引用变量cur呢?我之间用head头结点引用来不断的改变指向,完成结点间的链接,不可以吗?

可以是可以,但你有没有想过,如果用head来操作结点的化,你在初始化链表后head还指向头节点吗?你还能找到头结点吗


2.2 打印链表

  // 打印链表
    public void linkedListPrint() 
        ListNode cur = head;  // 创建一个引用cur去完成链表的遍历打印(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        while (cur != null)  
            System.out.print(cur.val + " "); // cur.val表示的就是当前结点的数值
            cur = cur.next;  // 打印完了当前结点,cur继续指向下一个结点,完成对下一个结点的打印
        
        System.out.println();
    


2.3 获取链表的长度:

  // 打印链表
    public void linkedListPrint() 
        ListNode cur = head;  // 创建一个引用cur去完成链表的遍历打印(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        while (cur != null)  
            System.out.print(cur.val + " "); // cur.val表示的就是当前结点的数值
            cur = cur.next;  // 打印完了当前结点,cur继续指向下一个结点,完成对下一个结点的打印
        
        System.out.println();
    

2.4 判断链表是否为空:

/**
     * 判断该链表是否为空
     * 为空就抛出异常,终止程序
     */
    private void isEmpty()  // 判断链表是否为空,只是该类中使用,所有
        if (head == null) 
            System.out.println("该链表为空!!!");
            throw new NullPointerException();
            // 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
            //异常一旦抛出,其后的代码就不会执行,相当于就直接return了
        
    


好了,现在我们就得到一个基本的链表了;

那么接下来就是对链表的操作了,那么一起来看看对链表都有那些操作吧!

 
    // 在在链表头插入元素
    public void addHead(int val)  
 
    //在链表的指定下标中插入元素
    public void addIndex(int index, int val)  
    // 删除头节点
    public void deleteHead()  
    //删除指定下标的元素
    public void deleteIndex(int index)  
 
    // 删除链表中所有数值是key的元素
    public void deleteKey(int key)  
 
    // 判断元素key是否在当前链表中
    public boolean contains(int key)  return false; 
 

三、新增结点

3.1头插:

// 在在链表头插入元素
    public void addHead(int val) 
        ListNode node = new ListNode(val); // 新插入的结点node
        node.next = head;    // 直接将该结点node变成新的头结点
        head = node;
    


3.2 指定下标插入

/**
     *  在链表的指定下标中插入元素
     * @param index 所指定的下标
     * @param val 元素值
     */
    public void addIndex(int index, int val) 
        if (index < 0 || index > getSize())  // 注意这里index == getSize也是可以的,此时相当于是在链表的结尾新增一个元素
            System.out.println("index下标不合法");
            return;
        
        ListNode node = new ListNode(val);  // 要新增的那个结点node
        if (index == 0)  // 当新增的是头结点的时候
            addHead(val);
            return;
        
        ListNode cur = head;
        for (int i = 0; i < index - 1; ++i)  // 这种情况包含了新增尾结点的时候
            cur = cur.next;                 //  通过循环,让cur指向—>新增指定下标所对应的元素的前一个元素
        
        node.next = cur.next;  // 把该结点插入链表,且放到指定下标中,从后往前走,先将node结点指向下一个结点
        cur.next = node;
    

举例说明:

比如现在我们想在2下标插入我们新建的结点node,那么我们首先要找到要插入的结点的前一个结点->也就是下标为1的那个结点

然后呢🤔

你可能会说:这不就简单了!直接下标为1的结点的next储存新建的结点node的地址:0x7777,然后新建的node结点再指向我们原来下标为2的结点不就行了吗?

这样真的可以吗?一起来看看吧!

上面所说的就是这样的伪代码:

下标为1的结点.next = node;
node.next = 下标为2的结点;
 
但这有一个问题:
首先在一开始的链表中存在这样的关系:
下标为1的结点.next = 下标为2的结点
 
那么就在上面的伪代码中node.next = 下标为2的结点;
就相等于是:node.next = 下标为1的结点.next;
但问题是此时的:下标为1的结点.next = node;
 
即node.next = node;这合理吗?不合理,所以我们要从后向前走
就是:
node.next = 下标为2的结点;
下标为1的结点.next = node;

所以说上面我们的那个想法是不合适的;


正确想法:

四、删除结点:

4.1 头删

// 删除头节点
    public void deleteHead() 
        isEmpty(); // 检查一下链表是否为空,为空的化就会抛出异常来终止程序
        // 如果head头节点不是链表的最后一个元素时,直接将head的下一个结点变成新的头结点,原来的head头结点就被系统自动回收了
        if (head.next != null) 
            this.head = this.head.next;
        
        else this.head = null;  // 当head结点是链表的最后一个元素时
    


4.2 指定下标的删除

在链表中,想要删除一个结点其实就是让该结点从链表中分离出来(没有其他任何的结点指向他 )

比如:

我们想删除1下标的结点,只需要找到1下标的前一个结点,也就是0下标。然后将0下标的next域里不再储存1下标的结点地址,而改成储存1下标的下一个结点->2下标的结点地址。这样就相等于1下标的结点被孤立下来了(就相等于是删除了)

/**
     * 删除指定下标的元素
     * @param index 要删除的指定元素下标
     */
    public void deleteIndex(int index) 
        isEmpty(); // 检查一下链表是否为空,为空的化就会抛出异常来终止程序
 
        if (index < 0 || index >= getSize())  // 注意此时index不能等于getSize因为是删除不是新增,下标index最大是getSize - 1
            System.out.println("要删除的下标不合法!删除失败!!");
            return; // 直接返回
        
        if (index == 0) 
            deleteHead(); // 当index等于0时,相当于是删除的是头节点
            return;
        
 
        ListNode cur = head;
        // 创建一个引用cur去完成循环(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        // 通过循环,让cur指向—>要删除元素的前一个元素
        for (int i = 0; i < index - 1; ++i)  // 注意这里不能是i < index; 如果是i < index的话,cur就指向当前要删除的那个元素了
            cur = cur.next;
        
        cur.next = cur.next.next;
    

图片说明:


4.3 删除链表中的指定元素

// 删除链表中所有数值是key的元素
    public void deleteKey(int key) 
        isEmpty(); // 检查一下链表是否为空,为空的化就会抛出异常来终止程序
        while (this.head.val == key && head != null)   // 当 head.val == key,相当于删除头节点
            deleteHead();            // 因为是删除链表中所有数值是key的元素,所以删除一个后不能直接返回,还要继续遍历
            // 处理特殊情况,当链表的的最后一个元素被删除时
            if (head == null) 
                return;  // 直接return就好,此时head为空,如果再进行this.head.val == key的判断就会发生空指针异常
            
        
 
        ListNode cur = head;
        // 创建一个引用cur去完成链表的遍历(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        while (cur.next != null) 
            if (cur.next.val == key) 
                cur.next = cur.next.next; // 包含了删除尾结点的情况
            
            else 
                cur = cur.next; // cur引用指向下一个结点,以此来完成遍历链表
            
        


 

五、单链表查找:

/**
     * 判断元素key是否在当前链表中
     * @param key
     * @return 在链表中返回true,不在返回false
     */
    public boolean contains(int key) 
        ListNode cur = this.head;
        while (cur != null) 
            if (cur.val == key) 
                return true;
            
            else 
                cur = cur.next;
            
        
        return false;
    


六、附录

总代码

//shift+回车,光标在任意位置都能换到下一行
// crl + z返回上一步,如果自己不小心误删了代码可以用这个快捷键找回刚才误删的代码
 
//import java.util.List;
import java.util.Scanner;
 
/**
 * 实现单链表的代码
 */
//变量名,方法名首字母小写,如果名称由多个单词组成,除首字母外的每个单词的首字母都要大写.
// 包名小写
public class MyLinkList 
    ListNode head = null; // 声明链表中的头结点
 
    //创建单链表结构,将链表的每一个结点都定义成一个内部类
    public class ListNode 
        public int val;        // 该结点的数值域,储存该结点的数值
        public ListNode next; // 该结点的next域,储存的是下一个结点的地址,两个结点间正是通过next域产生关联
 
        public ListNode(int val)  // 构造方法,给新生成的结点赋值,同时next默认为null
            this.val = val;
        
    
    // 链表初始化
    public ListNode listInit() 
        Scanner in = new Scanner(System.in);
        System.out.print("请输入你要构建的链表的初始长度:");
        int n = in.nextInt();
        System.out.print("请输入链表第1个元素的值:");
        int firstVal = in.nextInt();  //
        this.head = new ListNode(firstVal); // 创建链表的第一个结点,将我们链表的头结点指向链表的第一个结点
 
        ListNode cur = head; // 创建一个引用cur去完成链表的初始化(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        for (int i = 1; i < n; i++) 
            System.out.print("请输入链表第" + (i + 1) + "个元素的值:");
            int val = in.nextInt();
            ListNode node = new ListNode(val);
            cur.next = node;
            cur = node;
        
        return this.head; // 返回该链表的头结点
    
    // 打印链表
    public void linkedListPrint() 
        ListNode cur = head;  // 创建一个引用cur去完成链表的遍历打印(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        while (cur != null)  
            System.out.print(cur.val + " "); // cur.val表示的就是当前结点的数值
            cur = cur.next;  // 打印完了当前结点,cur继续指向下一个结点,完成对下一个结点的打印
        
        System.out.println();
    
    // 获取链表长度
    public int getSize() 
        int count = 0;
        ListNode cur = this.head;
        while (cur != null) 
            count++;
            cur = cur.next;
        
        return count;
    
 
    /**
     * 判断该链表是否为空
     * 为空就抛出异常,终止程序
     */
    private void isEmpty()  // 判断链表是否为空,只是该类中使用,所有
        if (head == null) 
            System.out.println("该链表为空!!!");
            throw new NullPointerException();
            // 如果抛出的是 RunTimeException 或者 RunTimeException 的子类,则可以不用处理,直接交给JVM来处理
            //异常一旦抛出,其后的代码就不会执行,相当于就直接return了
        
    
 
    // 在在链表头插入元素
    public void addHead(int val) 
        ListNode node = new ListNode(val);
        node.next = head;
        head = node;
    
 
    /**
     *  在链表的指定下标中插入元素
     * @param index 所指定的下标
     * @param val 元素值
     */
    public void addIndex(int index, int val) 
        if (index < 0 || index > getSize())  // 注意这里index == getSize也是可以的,此时相当于是在链表的结尾新增一个元素
            System.out.println("index下标不合法");
            return;
        
        ListNode node = new ListNode(val);  // 要新增的那个结点node
        if (index == 0)  // 当新增的是头结点的时候
            addHead(val);
            return;
        
        ListNode cur = head;
        for (int i = 0; i < index - 1; ++i)  // 这种情况包含了新增尾结点的时候
            cur = cur.next;                 //  通过循环,让cur指向—>新增指定下标所对应的元素的前一个元素
        
        node.next = cur.next;  // 把该结点插入链表,且放到指定下标中,从后往前走,先将node结点指向下一个结点
        cur.next = node;
    
 
    // 删除头节点
    public void deleteHead() 
        isEmpty(); // 检查一下链表是否为空,为空的化就会抛出异常来终止程序
        if (head.next != null) 
            this.head = this.head.next;
        
        else this.head = null;  // 当head结点是链表的最后一个元素时
    
 
    /**
     * 删除指定下标的元素
     * @param index 要删除的指定元素下标
     */
    public void deleteIndex(int index) 
        isEmpty(); // 检查一下链表是否为空,为空的化就会抛出异常来终止程序
 
        if (index < 0 || index >= getSize())  // 注意此时index不能等于getSize因为是删除不是新增,下标index最大是getSize - 1
            System.out.println("要删除的下标不合法!删除失败!!");
            return; // 直接返回
        
        if (index == 0) 
            deleteHead(); // 当index等于0时,相当于是删除的是头节点
            return;
        
 
        ListNode cur = head;
        // 创建一个引用cur去完成循环(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        // 通过循环,让cur指向—>要删除元素的前一个元素
        for (int i = 0; i < index - 1; ++i)  // 注意这里不能是i < index; 如果是i < index的话,cur就指向当前要删除的那个元素了
            cur = cur.next;
        
        cur.next = cur.next.next;
    
    // 删除链表中所有数值是key的元素
    public void deleteKey(int key) 
        isEmpty(); // 检查一下链表是否为空,为空的化就会抛出异常来终止程序
        while (this.head.val == key && head != null)   // 当 head.val == key,相当于删除头节点
            deleteHead();            // 因为是删除链表中所有数值是key的元素,所以删除一个后不能直接返回,还要继续遍历
            // 处理特殊情况,当链表的的最后一个元素被删除时
            if (head == null) 
                return;  // 直接return就好,此时head为空,如果再进行this.head.val == key的判断就会发生空指针异常
            
        
 
        ListNode cur = head;
        // 创建一个引用cur去完成链表的遍历(头节点是整个链表的灵魂,不能直接使用,避免丢失链表)
        while (cur.next != null) 
            if (cur.next.val == key) 
                cur.next = cur.next.next; // 包含了删除尾结点的情况
            
            else 
                cur = cur.next; // cur引用指向下一个结点,以此来完成遍历链表
            
        
    
 
    /**
     * 判断元素key是否在当前链表中
     * @param key
     * @return 在链表中返回true,不在返回false
     */
    public boolean contains(int key) 
        ListNode cur = this.head;
        while (cur != null) 
            if (cur.val == key) 
                return true;
            
            else 
                cur = cur.next;
            
        
        return false;
    

测试代码:

/**
 * 对单链表进行测试的代码
 */
public class LinkListTest 
    public static void main(String[] args) 
        MyLinkList myLinkList = new MyLinkList(); // 实例化一个链表对象
        // 链表的初始化
        myLinkList.listInit();
        System.out.print("初始化链表后,链表的第一次打印:");
        myLinkList.linkedListPrint();
 
        System.out.println("===============");
        myLinkList.addHead(1111);
        System.out.print("头插结点1111后,链表的第二次打印:");
        myLinkList.linkedListPrint();
 
        myLinkList.addIndex(2, 2222);
        System.out.print("再指定下标2出插入新结点2222后,链表的第三次打印:");
        myLinkList.linkedListPrint();
 
        System.out.println("================");
        myLinkList.deleteIndex(2);
        System.out.print("删除指定下标2的结点2222后,链表的第四次打印:");
        myLinkList.linkedListPrint();
 
        myLinkList.deleteHead();
        System.out.print("删除头节点后,链表的第五次打印:");
        myLinkList.linkedListPrint();
 
        myLinkList.deleteKey(2);
        System.out.print("删除链表中所有值是2的结点后,链表的第六次打印:");
        myLinkList.linkedListPrint();
        System.out.print("此时的链表长度为:");
        System.out.println(myLinkList.getSize());
    

测试结果:


总结

本节内容我们学习了有关链表中有关的各种操作,如插入删除等等;让我们堆链表有了更深刻的学习和认识;

 

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

⭐️本篇博客我要来和大家一起聊一聊数据结构初阶中的最后一篇博客——八大经典排序算法的总结,其中会介绍他们的原来,还有复杂度的分析以及各种优化。
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/data-structure/tree/master/Sort2.0


🌏排序总览

🍯什么是排序?

🍤 我们可以先了解一下两个概念:
🍤 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
🍤 排序的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

🍯为什么要排序?(作用)

💿排序的在生活中应用十分广泛,比如在我们刷抖音短视频的时候,大数据根据我们的喜好,会把我们喜欢的推送给我们,还有我们购物可以根据价格升降序之类的来选择商品等等。
💿所以说排序真的是十分的重要。

🍯排序的分类

🌏插入排序

🌴直接插入排序

🍇基本思想:把待排序的数逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
🍇一般地,我们把第一个看作是有序的,所以我们可以从第二个数开始往前插入,使得前两个数是有序的,然后将第三个数插入直到最后一个数插入。

我们可以先看一个动图演示来理解一下:

为了让大家更好地理解代码是怎么实现的,我们可以实现单趟的排序,代码如下:

int end = n-1;
// 先定义一个变量将要插入的数保存起来
int x = a[end + 1];
while (end >= 0)

	// 直到后面的数比前一个数大时就不往前移动,就直接把这个数放在end的后面
	if (a[end] > x)
	
		a[end + 1] = a[end];
		end--;
	
	else
	
		break;
	

a[end + 1] = x;

🍇前面我们也说了,是从第二个是开始往前插入,所以说第一趟的end应该为0,最后一趟的end应该是end = n - 2,根据end+1<n可以推出。

所以直接插入排序的整个过程的代码实现如下:

void InsertSort(int* a, int n)

	int i = 0;
	for (i = 0; i < n - 1; i++)
	
		int end = i;
		// 先定义一个变量将要插入的数保存起来
		int x = a[end + 1];
		// 直到后面的数比前一个数大时就不往前移动,就直接把这个数放在end的后面
		while (end >= 0)
		
			if (a[end] > x)
			
				a[end + 1] = a[end];
				end--;
			
			else
			
				break;
			
		
		a[end + 1] = x;
	

🍇时间复杂度和空间复杂度的分析
时间复杂度: 第一趟end最多往前移动1次,第二趟是2次……第n-1趟是n-1次,所以总次数是1+2+3+……+n-1=n*(n-1)/2,所以说时间复杂度是O(n^2)
最好的情况: 顺序
最坏的情况: 逆序
:给大家看一下直接插入排序排100w个数据要跑多久

空间复杂度:由于没有额外开辟空间,所以空间复杂度为O(1)
🍇直接插入排序稳定性的分析
直接插入排序在遇到相同的数时,可以就放在这个数的后面,就可以保持稳定性了,所以说这个排序是稳定的

🌴希尔排序

🍋基本思想:希尔排序是建立在直接插入排序之上的一种排序,希尔排序的思想上是把较大的数尽快的移动到后面,把较小的数尽快的移动到后面。先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。(直接插入排序的步长为1),这里的步长不为1,而是大于1,我们把步长这个量称为gap,当gap>1时,都是在进行预排序,当gap==1时,进行的是直接插入排序。
🍋可以先给大家看一个图解:

看一下下面动图演示的过程:

我们可以先写一个单趟的排序:

int end = 0;
int x = a[end + gap];
while (end >= 0)

	if (a[end] > x)
	
		a[end + gap] = a[end];
		end -= gap;
	
	else
	
		break;
	

a[end + gap] = x;

这里的单趟排序的实现和直接插入排序差不多,只不过是原来是gap = 1,现在是gap了。
由于我们要对每一组都进行排序,所以我们可以一组一组地排,像这样:

// gap组
for (int j = 0; j < gap; j++)

	int i = 0;
	for (i = 0; i < n-gap; i+=gap)
	
		int end = i;
		int x = a[end + gap];
		while (end >= 0)
		
			if (a[end] > x)
			
				a[end + gap] = a[end];
				end -= gap;
			
			else
			
				break;
			
		
		a[end + gap] = x;
	

也可以对代码进行一些优化,直接一起排序,不要一组一组地,代码如下:

int i = 0;
for (i = 0; i < n - gap; i++)// 一起预排序

	int end = i;
	int x = a[end + gap];
	while (end >= 0)
	
		if (a[end] > x)
		
			a[end + gap] = a[end];
			end -= gap;
		
		else
		
			break;
		
	
	a[end + gap] = x;

🍋当gap>1时,都是在进行预排序,当gap==1时,进行的是直接插入排序。
🍋gap越大预排越快,预排后越不接近有序
🍋gap越小预排越慢,预排后越接近有序
🍋gap==1时,进行的是直接插入排序。
🍋所以接下来我们要控制gap,我们可以让最初gap为n,然后一直除以2直到gap变成1,也可以这样:gap = gap/3+1。只要最后一次gap为1就可以了。
所以最后的代码实现如下:

void ShellSort(int* a, int n)

	int gap = n;
	while (gap > 1)// 不要写等于,会导致死循环
	
		// gap > 1 预排序
		// gap == 1 插入排序
		gap /= 2;
		int i = 0;
		for (i = 0; i < n - gap; i++)// 一起预排序
		
			int end = i;
			int x = a[end + gap];
			while (end >= 0)
			
				if (a[end] > x)
				
					a[end + gap] = a[end];
					end -= gap;
				
				else
				
					break;
				
			
			a[end + gap] = x;
		
	

🍋时间复杂度和空间复杂度的分析
时间复杂度: 外层循环的次数前几篇博客我们算过很多次类似的,也就是O(logN),
里面是这样算的

:给大家看一下直接插入排序排100w个数据要跑多久

看这时间,比起直接插入排序真的是快了太多。
空间复杂度:由于没有额外开辟空间,所以空间复杂度为O(1)
🍋希尔排序稳定性的分析
我们可以这样想,相同的数被分到了不同的组,就不能保证原有的顺序了,所以说这个排序是不稳定的

🌏选择排序

🌲直接选择排序

🍆基本思想每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
🍆我们先看一下直接选择排序的动图演示:

像上面一样,我们先来实现单趟排序:

int begin = 0;
int mini = begin;
int maxi = begin;
int i = 0;
for (i = begin; i <= end; i++)

	if (a[i] > a[maxi])
	
		maxi = i;
	
	if (a[i] < a[mini])
	
		mini = i;
	

// 如果maxi和begin相等的话,要对maxi进行修正
if (maxi == begin)
	maxi = mini;
Swap(&a[begin], &a[mini]);
Swap(&a[end], &a[maxi]);

这里我要说明一下,其中加了一段修正maxi的代码,就是为了防止begin和maxi相等时,mini与begin交换会导致maxi的位置发生变化,最后排序逻辑就会乱了,所以加上一段修正maxi的值得代码。

if (maxi == begin)
	maxi = mini;

整体排序就是begin往前走,end往后走,相遇就停下,所以整体代码实现如下:

void SelectSort(int* a, int n)

	int begin = 0;
	int end = n - 1;
	
	while (begin < end)
	
		int mini = begin;
		int maxi = begin;
		int i = 0;
		for (i = begin; i <= end; i++)
		
			if (a[i] > a[maxi])
			
				maxi = i;
			
			if (a[i] < a[mini])
			
				mini = i;
			
		
		// 如果maxi和begin相等的话,要对maxi进行修正
		if (maxi == begin)
			maxi = mini;
		Swap(&a[begin], &a[mini]);
		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	
 

🍆时间复杂度和空间复杂度的分析
时间复杂度: 第一趟遍历n-1个数,选出两个数,第二趟遍历n-3个数,选出两个数……最后一次遍历1个数(n为偶数)或2个数(n为奇数),所以总次数是n-1+n-3+……+2,所以说时间复杂度是O(n^2)
最好的情况: O(n^2)(顺序)
最坏的情况: O(n^2)(逆序)
直接选择排序任何情况下的时间复杂度都是 O(n^2),因为不管有序还是无序都要去选数。
🍆给大家看一下直接选择排序排100w个数据要跑多久

空间复杂度:由于没有额外开辟空间,所以空间复杂度为O(1)
🍆直接选择排序稳定性的分析
我们可以这样想

所以说直接选择排序是不稳定的

🌲堆排序

🌽堆排序我在上上一篇博客已经详细介绍了,大家可以点击这里去看堆排序

🌏交换排序

🍅基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

🐚冒泡排序

🍅基本思想:它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
🍅图解如下:

🍅再看一个冒泡排序的动图:

先实现单趟冒泡排序:

int j = 0;
for (j = 0; j < n - 1; j++)

	// 比后面的数大就交换
	if (a[j] > a[j + 1])
	
		exchange = 1;
		Swap(&a[j], &a[j + 1]);
	

再实现整体的排序:

void BubbleSort(int* a, int n)

	int i = 0;
	for (i = 0; i < n - 1; i++)
	
		int exchange = 0;
		int j = 0;
		for (j = 0; j < n - i - 1; j++)
		
			if (a[j] > a[j + 1])
			
				exchange = 1;
				Swap(&a[j], &a[j + 1]);
			
		
	

🍅我们再考虑这样一个问题,假如当前的序列已经有序了,我们有什么办法让这个排序尽快结束吗?
这当然是有的,我们可以定义一个exchange的变量,如果这趟排序发生交换就把这个变量置为1,否则就不变,不发生交换的意思就是该序列已经有序了,利用这样一个变量我们就可以直接结束循环了。

优化后的代码如下:

void BubbleSort(int* a, int n)

	int i = 0;
	for (i = 0; i < n - 1; i++)
	
		int exchange = 0;
		int j = 0;
		for (j = 0; j < n - i - 1; j++)
		
			if (a[j] > a[j + 1])
			
				exchange = 1;
				Swap(&a[j], &a[j + 1]);
			
		
		// 不发生交换
		if (exchange == 0)
			break;
	

🍅时间复杂度和空间复杂度的分析
时间复杂度: 第一趟最多比较n-1次,第二趟最多比较n-2次……最后一次最多比较1次,所以总次数是n-1+n-2+……+1,所以说时间复杂度是O(n^2)
最好的情况: O(n)(顺序)
最坏的情况: O(n^2)(逆序)
所以说冒泡排序在最好的情况下比直接选择排序更优。
🍅给大家看一下冒泡排序排10w个数据要跑多久,因为太慢了,所以这里只排10w

可以看出的是,10w个数冒泡排序都排的很久。
空间复杂度:由于没有额外开辟空间,所以空间复杂度为O(1)
🍅直接选择排序稳定性的分析
冒泡排序在比较遇到相同的数时,可以不进行交换,这样就保证了稳定性,所以说冒泡排序数稳定的

🐚快速排序(递归版本)

🌰快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。

🍍hoare版本

🌰基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
🌰我们先看一个分割一次的动图:
🌰我们要遵循一个原则:关键词取左,右边先找小再左边找大;关键词取右,左边找先大再右边找小
🌰一次过后,2也就来到了排序后的位置,接下来我们就是利用递归来把key左边区间和右边的区间递归排好就可以了,如下:

递归左区间:[left, key-1] key 递归右区间:[key+1, right]

hoare版本找key值代码实现如下:

int PartSort1(int* a, int left, int right)

	int keyi = left;
	while (left < right)
	
		// 右边找小
		while (left < right && a[right] >= a[keyi])
		
			right--;
		

		// 左边找大
		while (left < right && a[left] <= a[keyi])
		
			left++;
		
		Swap(&a[left], &a[right]);
	
	Swap(&a[keyi], &a[left]);

	return left;

快排代码实现如下:

void QuickSort(int* a, int left, int right)

	if (left > right)
		return;

	int div = PartSort1(a, left, right);

	// 两个区间 [left, div-1] div [div+1, right]
	QuickSort(a, left, div - 1);
	QuickSort(a, div + 1, right);

🌰我们考虑这样一种情况,当第一个数是最小的时候,顺序的时候会很糟糕,因为每次递归right都要走到头,看下图:

此时会建立很多函数栈帧,递归的深度会很深,会导致栈溢出(stackover),看下图:

为了优化这里写了一个三数取中的代码,三数取中就是在序列的首、中和尾三个位置选择第二大的数,然后放在第一个位置,这样就防止了首位不是最小的,这样也就避免了有序情况下,情况也不会太糟糕。
下面是三数取中代码:

int GetMidIndex(int* a, int left, int right)

	int mid = left + (right - left) / 2;
	if (a[mid] > a[left])
	
		if (a[right] > a[mid])
		
			return mid;
		
		// a[right] <= a[mid]
		else if (a[left] > a[right])
		
			return left;
		
		else
		
			return right;
		
	
	// a[mid] <= a[left]
	else
	
		if (a[mid] > a[right])
		
			return mid;
		
		// a[mid] <= a[right]
		else if (a[left] > a[right])
		
			return right;
		
		else
		
			return left;
		
	

所以加上三数取中优化后的代码如下:

int PartSort1(int* a, int left, int right)

	int index = GetMidIndex(a, left, right);
	Swap(&a[index], &a[left]);
	int keyi = left;
	while (left < right)
	
		// 右边找小
		while (left < right && a[right] >= a[keyi])
		数据结构初阶第四篇——双链表(实现+图解)

C++初阶第四篇——类和对象(上)(类的定义+封装+this指针)

初阶数据结构——线性表——链表——不带头单向链表

初阶数据结构——线性表——链表——带头双向循环链表

数据结构初阶第五篇——栈和队列(实现+图解)

数据结构初阶第二篇——顺序表(实现+动图演示)[建议收藏]