09考研数据结构试题的一种解法(Java版)

本文提出了一种高效算法,用于在单向链表中找出倒数第K个节点,该算法的时间复杂度为O(n),空间复杂度为O(1)。通过巧妙地利用两次遍历和双指针技巧,确保了算法的高效性和简洁性。
http://www.blogjava.net/nokiaguy/archive/2009/02/archive/2009/nokiaguy/archive/2009/nokiaguy/archive/2009/01/17/251722.html
本文为原创,如需转载,请注明作者和出处,谢谢!   

    虽然研究生已毕业,但看到有一些难度的研究生考试题还是忍不住要做做,本文给出了09年研究生入学考试的一道数据结构题的Java实现。该题的描述如下图所示。

09考研数据结构试题的一种解法(Java版)  - 清风幻影 - 清风侠的博客

    该题的两种实现一位朋友已经完成了,详见 递归和非递归实现 。在本文将给出另外一种算法,该算法的空间复杂度为O(1),时间复杂度为O(n)。这在空间复杂度和时间复杂度上应该是比较优化了。
    本算法的基本思想如下:
    既然是查找倒数第K个结点(注意,不是正数,否则就没什么可讨论的了),而且链表是单向的,还不能改变表结构,这就意味着只能从前往后扫描结点。我们首先 要知道这个链表有多少个结点(如果总结点数都不知道,何谈倒数?),这个非常简单,只要从头扫描一下链表,再计一下数即可。
    在同一时间从事多项工作会大大提升效率,当然,扫描链表也不例外,在扫描链表的同时,还需要做一些其他的工作。既然只能从前向后扫描链表,而且要求倒数第 K个结点,那就让我们把这个链表按长度为K分成若干块,而最后扫描的结果要么结点数是K的整数倍(模为0),要么余数(模)不为0(多出几个结点,多出的 结点数小于K)。
    先看看第二种情况。
    假设有12个结点的链表,每一个结点的值从前往后分别是1至12,如下所示:

    1  2  3  4  5  6  7  8  9  10  11 12

    假设我们要求倒数第5个结点,我们直接就可以看出结果是8。那么用程序如何处理呢?
   
    先按长度为5将上面的结点分成三个区域,如下:

    1 2 3 4 5           6 7 8 9 10           11 12

    注意,不是物理分,而是使用变量来保存区域的边界(也就是区域最后一个结点的对象)。
    从上面的分隔可以看出,最后剩下两个结点,既然是求倒数第5个,而最后剩下了两个,那么还缺5-2=3个,因此,只需要从倒数第二个块(6 7 8 9 10)略过前两个,第三个结点(8)就是我们要求的结果,而5就是题中的k,2就是结点数与k的模,因此,可以推出一个公式,倒数第k个结点就是按长度为 k按分成的若干块中的第二块的第(结点数 % k+ 1)个结点。
    下面来看看(结点数 % k)为0的情况。假设上面的例子中的k为4,正确的输出结果应为9,分块如下:

    1 2 3 4      5 6 7 8      9 10 11 12

    从上面的三个块可以看出,结果正好是最后一个块的第一个结点,这时mod为0(mod=结点数 % k),因此,在这种情况也可以使用上面的公式,只是变成了最后一个块。

    根据上面的基本思想可以设两个指针,p1和p2,其中p1最终指向倒数第2个完整块,p2最终指向倒数第1个完整块,对于第一种情况,p1指向5,p2指 向10,这时可以使p1向后移动(k - mod)个结点即可(从5移动3个正好是8)。而对于第二种情况,p1指向8,p2指向12,而mod=0,这时的结果仍然是mod+1,也就是p1向后 移动1个结点就是所求的结果。 为了满足(k=结点数)的情况,需要将p1的初始值设为头结点,这样如果(k=结点数),就直接从头结点向后移动一个结点就是最后的结果,如上面的例子求 倒数第12个结点,也就是求正数第1个结点。

    下面是这个算法的具体实现(包括核心算法、生成链表及调用核心算法的代码):

public   class  Test
{

    
static   class  Node
    {
        
public   int  data;
        
public  Node nextNode;
    }
    
//////////////////////////////////////////
    
//   核心算法
     private   static   int  findNode(Node headNode,  int  k)
    {
        Node p 
=  headNode, p1  =  headNode, p2  =   null
        
int  count  =   0 ;   //   表示结点数
         while  (p.nextNode  !=   null )
        {
            p 
=  p.nextNode;
            count
++ ;
            
//   遇到k的整数位个结点,进行分块
             if  (count  %  k  ==   0 )
            {                
                
if  (p2  !=   null )
                    p1 
=  p2;
                p2 
=  p;
            }
        }
        
//   k超过链表结点数,未找到,返回0
        // 此处也可以用k > count来判断
         if  (p2  ==   null
        {
            
return   0 ;
        }
        
else
        {
            
int  mod  =  count  %  k;
            
int  offset  =  mod  +   1 ;   //  任何情况下,最终结果都是p1指向的结点向后移动(mod + 1)个结点
             for  ( int  i  =   0 ; i  <  offset; i ++ )
                p1 
=  p1.nextNode;
            System.out.println(p1.data);
            
return   1 ;
        }
    }
    
////////////////////////////////////////
     public   static   void  main(String[] args)  throws  Exception
    {
        
// 产生一个包含1个头结点和120个结点的链表
        Node headNode  =   new  Node();
        Node p 
=  headNode;
        
for  ( int  i  =   0 ; i  <   120 ; i ++ )
        {
            p.nextNode 
=   new  Node();
            p.nextNode.data 
=  i  +   1 ;
            p 
=  p.nextNode;
        }
        p.nextNode 
=   null ;
        
//   开始查找倒数第k个结点,如果找到,返回1,并输出结点的data值
        System.out.println(findNode(headNode,  12 ));
    }
}

    上面程序的输出结果如下:

    109
    1

    读者也可以使用其他的测试用例来测试上面的程序。

    本算法从空间复杂度O(1)和时间复杂度O(n)的综合指标基本上算是比较优化了,如果哪位读者还有更简单和快速的算法,请跟贴!!
跟帖:
一个指针先跑到第k个元素,然后另一个指针从头开始,两个指针同步往后走,直到第一个指针碰到尾部,第二个指针就是倒数第k个位置了。特殊情况(元素个数少于k等)再处理下就行了。

空间复杂度也是O(1)

这个方法不错,下面是具体的实现代码:

    private static int findNode(Node headNode, int k)
    {       
        Node p1 = headNode, p2 = headNode;
        for(int i = 0; i < k && p2.nextNode != null; i++)
            p2 = p2.nextNode;
        if(p2.nextNode == null) return 0;
        while(p2.nextNode != null)
        {
            p1 = p1.nextNode;
            p2 = p2.nextNode;
        }
        System.out.println(p1.nextNode.data);
        return 1;
    }
1.1 单项选择题 1. 数据结构是一门研究非数值计算的程序设计问题中,数据元素的① 、数据信息在计算机中的② 以及一组相关的运算等的课程。 ① A.操作对象   B.计算方法  C.逻辑结构  D.数据映象 ② A.存储结构 B.关系 C.运算 D.算法 2. 数据结构DS(Data Struct)可以被形式地定义为DS=(D,R),其中D是① 的有限集合,R是D上的② 有限集合。 ① A.算法 B.数据元素 C.数据操作 D.数据对象 ② A.操作 B.映象 C.存储 D.关系 3. 在数据结构中,从逻辑上可以把数据结构分成 。 A.动态结构和静态结构 B.紧凑结构和非紧凑结构 C.线性结构和非线性结构 D.内部结构和外部结构 4. 算法分析的目的是① ,算法分析的两个主要方面是② 。 ① A. 找出数据结构的合理性 B. 研究算法中的输入和输出的关系 C. 分析算法的效率以求改进 D. 分析算法的易懂性和文档性 ② A. 空间复杂性和时间复杂性 B. 正确性和简明性 C. 可读性和文档性 D. 数据复杂性和程序复杂性 5. 计算机算法指的是① ,它必具备输入、输出和② 等五个特性。 ① A. 计算方法 B. 排序方法 C. 解决问题的有限运算序列 D. 调度方法 ② A. 可行性、可移植性和可扩充性 B. 可行性、确定性和有穷性 C. 确定性、有穷性和稳定性 D. 易读性、稳定性和安全性 1.2 填空题(将正确的答案填在相应的空中) 1. 数据逻辑结构包括 、 、 和 四种类型,树形结构和图形结构合称为 。 2. 在线性结构中,第一个结点 前驱结点,其余每个结点有且只有 个前驱结点;最后一个结点 后续结点,其余每个结点有且只有 个后续结点。 3. 在树形结构中,树根结点没有 结点,其余每个结点有且只有 个直接前驱结点,叶子结点没有 结点,其余每个结点的直接后续结点可以 。 4. 在图形结构中,每个结点的前驱结点数和后续结点数可以 。 5. 线性结构中元素之间存在 关系,树形结构中元素之间存在 关系,图形结构中元素之间存在 关系。 6. 算法的五个重要特性是__ __ , __ __ , ___ _ , __ __ , _ ___。 7. 分析下面算法(程序段),给出最大语句频度 ,该算法的时间复杂度是__ __。 for (i=0;i<n;i++) for (j=0;j<n; j++) A[i][j]=0; 8. 分析下面算法(程序段),给出最大语句频度 ,该算法的时间复杂度是__ __。 for (i=0;i<n;i++) for (j=0; j<i; j++) A[i][j]=0; 9. 分析下面算法(程序段),给出最大语句频度 ,该算法的时间复杂度是__ __。 s=0; for (i=0;i<n;i++) for (j=0;j<n;j++) for (k=0;k<n;k++) s=s+B[i][j][k]; sum=s; 10. 分析下面算法(程序段)给出最大语句频度 ,该算法的时间复杂度是__ __。 int i=0,s=0; while (s<n) { i++; s+=i; //s=s+i } 11. 分析下面算法(程序段)给出最大语句频度 ,该算法的时间复杂度是__ __。 i=1; while (i<=n) i=i*2;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值