【线性表】处理线性表元素的算法思想

本文介绍数组的多下标思想和链表的快慢指针思想,通过具体实例讲解这两种思想的应用,如数组Partition、荷兰国旗问题以及链表中间值获取、环检测等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数组是数据结构中最核心、最重要、最变化多端的数据结构,如何操作数组的元素,成为了基于数组存储结构的算法研究方向之一。本文主要说明在操作数组元素时的两种思想,这不是新事物,只是一种笔者在学习过程中的一种总结归纳,希望对读者有益!

目录

数据结构的增删改查思想

数组中的多下标思想

实例1-数组Partition

实例2-荷兰国旗问题

链表中的快慢指针思想

用最快的方法获取单链表的中间值

判断单链表中是否有环

单链表中环的入口

辅助的数组、链表思想


数据结构的增删改查思想

在关系型数据库中,不管是对于数据库本身,还是对于表,都有且仅有四种基本的操作:增删改查,其他的操作都是这四种基本操作的变形或是综合。

将这种思想引入到数据结构中:数据结构中的“结构”,具有四种增、删、改、查四种基本的操作,其他的高级操作都是这四种基本操作的变形与综合。其中,“增”的含义是:创建、插入、新增,“删”的含义是:删除元素,“改”的含义是:修改,“查”的含义是:查找,查询。

例如:在一个数组(一种线性顺序表)中,在指定的下标中插入元素涉及到的操作有:查找,移动(修改),插入,共计三种基本操作。

于是,学习任意一种数据结构,只要牢牢把握住这种“结构”的增删改查四种基本操作,其他的操作都是四种基本数据类型的变形与综合!

 

数组中的多下标思想

背景:

  • 数组中每个元素都有一个下标,可以通过该下标对元素直接操作,这也叫做随机访问
  • 数组的所有操作都是通过下标来进行的。例如,遍历数组通过一个从0开始到数组长度LEN的区间[0, LEN)的遍历下标来实现的。
  • 那实现以下复杂的操作,如在一次遍历中就实现大元素放右边,小元素放左边,该如何实现?答案是引入多个下标,同时对数组元素进行操作。

多下标的类型:

  • 从两端向中间的双下标
  • 从头开始且相邻的双下标
  • 快慢下标思想(又叫快慢指针思想)
  • 三个及三个以上下标

实例1-数组Partition

问题:

给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。

算法思想:

  1. 下标x的含义是:下标为0~x之间的数全部是小于等于num的,也就是该区域、范围里的数是小于等于num的;初始时x的值为-1,表示还不存在有小于等于num的范围。
  2. 遍历:如果遇到a[i]≤num,则把a[i]和上述区域内的最后一个值交换,然后再扩大上述区域;如果遇到a[i]>num,直接跳到下一个位置,即i++;

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

void exchange(int* a, int len, int num)
{
	int i; //遍历使用的下标
	int x; //[0,x]区域内的所有数都小于等于num
	int t; //交换两个数时使用的临时变量

	x = 0;//x = -1; //初始时小于等于num的区域不存在
	for (i = 0; i < len; i++)
	{
		if (a[i] < num)
		{
			t = a[i]; a[i] = a[x]; a[x] = t;
			x += 1; //属于小于等于num的区域扩大
		}
	}
}

int main()
{
	int len = 5;
	int num = 5;
	int i;

	scanf("%d", &len);
	int* a = (int*)malloc(sizeof(int) * len);
	for (i = 0; i < len; i++)
	{
		scanf("%d", &a[i]);
	}
	scanf("%d", &num);

	exchange(a, len, num);
	for (i = 0; i < len; i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
}

实例2-荷兰国旗问题

问题:

给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

算法思想:(三下标思想)

遍历过程中:

  1.  如果发现a[cur]小于num,则直接把a[cur]与[0, less]区域里的最后一个数交换,然后less++cur++(也就是小于num的区域内的数量增加,又继续看下一个数);
  2.  如果a[cur]等于num,则什么都不做,直接看下一个;
  3.  如果a[cur]>num,则把a[cur]和[more, N-1]区域的第一个数交换,然后,more--(也就是more向前扩一个位置);然后看换过来的那个数,是等于num的?还是小于num的?再执行“①②”中所提到的操作;
  4.  当cur等于more相等的时候,表示停止操作。初始时less=-1,more=N。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

void NetherlandsFlag(int* a, int L, int R, int num)
{
	int less = L - 1;
	int more = R + 1;
	int cur = L;
	int t;

	while (cur < more)
	{
		if (a[cur] < num) //对应算法思想中的①操作
		{
			less++; //因为初始时less=L-1,当L=0时,less=-1;若不先加一,则a[-1]不合法!
			t = a[less]; a[less] = a[cur]; a[cur] = t;
			cur++;
		}
		else if (a[cur] > num) //对应算法思想中的③操作
		{
			more--;
			t = a[cur]; a[cur] = a[more]; a[more] = t;
		}
		else  //对应算法思想中的②操作
		{
			cur++;
		}
	}
	//最终的等于区域是:下标为[less+1, more-1]
}

int main()
{
	int len = 5;
	int num = 5;
	int i;

	scanf("%d", &len);
	int* a = (int*)malloc(sizeof(int) * len);
	for (i = 0; i < len; i++)
	{
		scanf("%d", &a[i]);
	}
	scanf("%d", &num);

	NetherlandsFlag(a, 0, len-1, num);
	for (i = 0; i < len; i++)
	{
		printf("%d ", a[i]);
	}

	return 0;
}

链表中的快慢指针思想

对于链表来说,它没有数组那样的随机访问,只能从头到尾或者从尾到头依次遍历,那如何实现更加复杂的操作呢?答案还是引入多个遍历指针。

为了简化问题,专注于对思想的理解,我们以单链表为例,理解链表中的快慢指针。

用最快的方法获取单链表的中间值

利用快慢指针思想获取链表的中间值:

  • 定义一个快指针pa,一个慢指针pb;快指针比慢指针快两倍;
  • 同时遍历链表,等快指针到链表末尾时,就意味着慢指针到链表中间;

快慢指针获取中间值
public class MainTest {
    public static void main(String[] args) throws Exception {
        Node<String> first = new Node<String>("aa", null);
        Node<String> second = new Node<String>("bb", null);
        Node<String> third = new Node<String>("cc", null);
        Node<String> fourth = new Node<String>("dd", null);
        Node<String> fifth = new Node<String>("ee", null);
        Node<String> six = new Node<String>("ff", null);
        Node<String> seven = new Node<String>("gg", null);

        //完成结点之间的指向
        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;

        //查找中间值
        String mid = getMiddle(first);
        System.out.println("中间值为:"+mid);
    }

    public static String getMiddle(Node<String> head) {
        Node<String> fast = head;
        Node<String> slow = head;
        while(fast.next!=null && slow.next!=null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow.data;
    }

    private static class Node<T> {
        T data;
        Node<T> next;
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}

判断单链表中是否有环

利用快慢指针思想判断单链表是否有环的问题:

主要思想:

  1. 设置一个快指针pa,一个慢指针pb,(这里假定快指针比慢指针快两倍);
  2. 当慢指针的遍历完毕了整个链表,都没有慢指针的值等于快指针的值时,则说明此链表无环;
  3. 反之,当慢指针遍历链表的过程中,发生的快慢指针的值相等的情况时,则说明此链表有环!
快慢指针判断是否有环
public class MainTest {
    public static void main(String[] args) throws Exception {
        Node<String> first = new Node<String>("aa", null);
        Node<String> second = new Node<String>("bb", null);
        Node<String> third = new Node<String>("cc", null);
        Node<String> fourth = new Node<String>("dd", null);
        Node<String> fifth = new Node<String>("ee", null);
        Node<String> six = new Node<String>("ff", null);
        Node<String> seven = new Node<String>("gg", null);

        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;
        //产生环
        seven.next = third;

        boolean circle = isCircle(first);
        System.out.println("first链表中是否有环:"+circle);
    }

    public static boolean isCircle(Node<String> first) {
        //定义快慢指针
        Node<String> fast = first;
        Node<String> slow = first;

        //遍历链表,如果快慢指针指向了同一个结点,那么证明有环
        while(fast!=null && fast.next!=null){
            //变换fast和slow
            fast = fast.next.next;
            slow = slow.next;

            if (fast.equals(slow)){
                return true;
            }
        }

        return false;
    }

    private static class Node<T> {
        T data;
        Node<T> next;
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}

单链表中环的入口

当快慢指针相遇时,我们可以判断到链表中有环,这时重新设定一个新指针指向链表的起点,且步长与慢指针一样为1,则慢指针与“新”指针相遇的地方就是环的入口。证明这一结论牵涉到数论的知识,这里略,只讲实现。

检测环的入口
public class MainTest {
    public static void main(String[] args) throws Exception {
        Node<String> first = new Node<String>("aa", null);
        Node<String> second = new Node<String>("bb", null);
        Node<String> third = new Node<String>("cc", null);
        Node<String> fourth = new Node<String>("dd", null);
        Node<String> fifth = new Node<String>("ee", null);
        Node<String> six = new Node<String>("ff", null);
        Node<String> seven = new Node<String>("gg", null);

        first.next = second;
        second.next = third;
        third.next = fourth;
        fourth.next = fifth;
        fifth.next = six;
        six.next = seven;
        //产生环
        seven.next = third;

        System.out.println("有环链表中的入口:"+getCircleEntrance(first).data);
    }

    public static Node<String> getCircleEntrance(Node<String> first) {
        Node<String> fast = first;//定义快慢指针
        Node<String> slow = first;
        Node<String> temp = null;

        //遍历链表,先找到环(快慢指针相遇),准备一个临时指针,指向链表的首结点,继续遍历,
        // 直到慢指针和临时指针相遇,那么相遇时所指向的结点就是环的入口
        while(fast!=null && fast.next!=null){
            fast = fast.next.next;
            slow = slow.next;

            //判断快慢指针是否相遇
            if (fast.equals(slow)){
                temp = first; //让辅助指针指向首节点
                continue;
            }

            //让辅助结点前进
            if (temp!=null){
                temp = temp.next;
                //判断临时指针是否和慢指针相遇
                if (temp.equals(slow)){
                    break;
                }
            }
        }

        return temp;
    }

    private static class Node<T> {
        T data;
        Node<T> next;
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }
}

辅助的数组、链表思想

其实就是用空间换时间的思想,将时间复杂度转化到了空间复杂度上。

例如:在上文的数组Partition实例中,我们可以开辟两个新的数组M、N,把大于num的元素放在M中,把小于num的元素放在N中,最后将N、M中的元素依次又复制回去。但这种做法并不实用!

总结:

  1. 如果是数组,可以使用多下标的思想解决问题。快慢下标、从两端向中间的双下标、多下标等。灵活地使用多个下标就能高效地解决问题。
  2. 如果是链表,可以使用快慢指针的思想解决问题。快指针比慢指针快多少取决于具体的场景。

     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值