Amol Pawar

Java Interface

Mastering Java Interfaces: A Comprehensive Guide for Developers

Java interfaces are an essential aspect of the Java programming language. They provide a way to achieve abstraction, multiple inheritance, and polymorphism in Java. In this comprehensive guide, we will delve into the intricacies of Java interfaces, covering their definition, purpose, usage, best practices, and examples.

What is a Java Interface?

Any Service Requirement Specification (SRS) is considered an interface. For example, the JDBC API acts as a requirement specification to develop database drivers, and database vendors are responsible for implementing this JDBC API. Servlet API is an SRS, and web server vendors implement it.

From the client’s point of view, an interface is a set of services that are expected. From the service provider’s perspective, an interface is the set of services offered to the client. Hence, an interface is a contract between the client and the service provider. For instance, the Bank ATM GUI Screen serves as a contract between the bank and the customer, with services like withdrawal, mini-statement, and balance inquiry.

Inside an interface, every method is always abstract whether we declare it explicitly or not. Therefore, an interface is considered a 100% pure abstract class. Whenever we implement an interface, we must provide implementations for every method in the java interface. If we are unable to do so, we declare the implementing class as abstract since it doesn’t contain all method implementations. Additionally, we need to define methods as public so that the next level child class can provide implementations for those methods.

Defining a Java Interface

Java interfaces are defined using the interface keyword followed by the interface name. Here’s an example of a simple interface declaration:

Java
interface Animal {
    void eat();
    void sleep();
}

In this example, the Animal interface defines two abstract methods: eat() and sleep(). Any class that implements the Animal interface must provide implementations for these methods.

Implementing Java Interface

To implement an interface in Java, a class uses the implements keyword followed by the name of the interface. Here’s an example of a class implementing the Animal interface:

Java
class Dog implements Animal {
    public void eat() {
        System.out.println("Dog is eating");
    }
    
    public void sleep() {
        System.out.println("Dog is sleeping");
    }
}

In this example, the Dog class implements the Animal interface by providing implementations for the eat() and sleep() methods.

When comparing “extends” and “implements”:

a) A class can extend only one class at a time, but an interface can extend any number of interfaces. For example:

Java
interface A {}
interface B {}
interface C extends A, B {}

b) A class can extend another class and implement any number of interfaces. For instance:

Java
class A extends B implements C, D, E {}

c) An interface never implements another interface or interfaces; it only extends interfaces. Therefore, the statement “interface A implements B” is incorrect.

Consider the following scenarios:

  1. X extends Y: Both X and Y can be either classes or interfaces.
  2. X extends Y, Z: X, Y, and Z should be interfaces.
  3. X implements Y, Z: X should be a class, and Y, Z should be interfaces.
  4. X extends Y implements Z: X and Y are classes, and Z is an interface.
  5. X implements Y extends Z: This would result in a compile error because “extends” must be specified before “implements.”

Java Interface Methods

Every method present in java interface is always public and abstract, whether declared explicitly or not. Hence, inside an interface, the following method declarations are equivalent:

Java
void m1();
public void m1();
abstract void m1();
public abstract void m1();
abstract public void m1();

In an interface Interf, the method declaration would be:

Java
interface Interf {
    public abstract void m1();
}

Here the use of public and abstract:

public: This modifier is used to make the method available to every implementation class.

abstract: This modifier signifies that the implementation class is responsible for providing the implementation.

Since every method in an interface is public and abstract, the following modifiers cannot be used in an interface:

private, protected, static, final, synchronized, strictfp, native.

Java Interface Variables

To define constant values at the requirement level, we use variables inside an interface. Every variable inside an interface is always public, static, and final, regardless of whether we explicitly declare these modifiers.

public: This modifier is used to make the variable available to every implementation class.

static: This modifier allows access to the variable without needing an existing object of the implementing class.

final: This modifier ensures that if one implementation class changes the value, all other implementation classes will be affected. To prevent this, every interface variable is declared as final.

Hence, the following declarations are equivalent inside an interface:

Java
int x = 10;
public int x = 10;
static int x = 10;
final int x = 10;
public static int x = 10;
public final int x = 10;
static final int x = 10;
public static final int x = 10;

As every interface variable is public, static, and final, we cannot declare interface variables with the following modifiers:

private, protected, transient, volatile.

It is mandatory to initialize interface variables at the time of declaration; otherwise, we will encounter a compilation error. For example:

Java
interface Interf {
    int x; // CE: = expected.
}

Inside a class implementing an interface, we can access interface variables, but we cannot modify them. Attempting to do so will result in a compilation error. For example:

Java
interface Interf {
    int x = 10;
}

class Test implements Interf {
    public static void main(String args[]) {
        x = 777; // CE: cannot assign a value to final variable x
        System.out.println(x);   
    }
}

However, if we declare int x = 777; instead of x = 777;, it is perfectly valid because it acts like a local variable and not an interface variable.

Java Interface Naming Conflicts

Method Naming Conflicts:

When dealing with method naming conflicts in interfaces, the resolution depends on various factors including method signature, return type, and argument types.

Same Signature, Same Return Type

If two interfaces contain methods with the same signature and return type, then in the implementing class, we have to provide implementation for only one method.

Java
interface Left {
    public void m1();
}

interface Right {
    public void m1();
}

class Test implements Left, Right {
    public void m1() {}
}
Same Name, Different Argument Types (Overloaded Methods)

If two interfaces contain methods with the same name but different argument types, then in the implementation class, we have to provide implementation for both methods, and these methods act as overloaded methods.

Java
interface Left {
    public void m1();
}
interface Right {
    public void m1(int i);
}

class Test implements Left, Right {
    public void m1() {
        // Implementation
    }

    public void m1(int i) {
        // Implementation
    }
}
Same Signature, Different Return Types

If two interfaces contain methods with the same method signature but different return types, then it is impossible to implement both interfaces simultaneously. However, in the case of general or covariant return types, it is possible.

Java
interface Left {
    public void m1();
}

interface Right {
    public int m1();
}

// It is impossible to implement both interfaces having the same method signature but different return types.
// In the case of general or covariant return types, it is possible.
class Test implements Left, Right {
    public void m1() {}   // Impossible
    
    public int m1() {}    // Impossible
}

When facing method naming conflicts in interface implementation, understanding the differences in method signatures, return types, and argument types helps determine the appropriate approach for implementation.

Java Interface Variable Naming Conflicts

When dealing with variable naming conflicts in java interfaces, if two interfaces contain variables with the same names, attempting to use the variable directly inside an implementing class can lead to a naming conflict error, resulting in a compilation error.

Java
interface Left {
    int x = 777;
}
interface Right {
    int x = 888;
}

class Test implements Left, Right {
    public static void main(String[] args) {
        System.out.println(x); // Compilation Error: reference to x is ambiguous

        // Solution: Resolve naming conflict using interface names
        System.out.println(Left.x);  // Output: 777
        System.out.println(Right.x); // Output: 888
    }
}

To resolve this issue, you can specify the interface name along with the variable name to uniquely identify which interface’s variable you are referring to. By doing so, you can access the variables without ambiguity and prevent compilation errors.

Marker Interface

Definition and Purpose

A marker interface, also known as an ability interface or tag interface, is an interface that does not contain any methods. Instead, it serves as a marker or tag to indicate that objects implementing the interface possess certain abilities or characteristics.

Examples of marker interfaces in Java include Serializable, Cloneable, RandomAccess, and SingleThreadModel. These interfaces indicate that objects implementing them have specific abilities or properties related to serialization, cloning, random access, or single-threaded access.

Java
interface Serializable {
}
Mechanism (Without having any methods how marker interface provide some ability?)

Even though marker interfaces do not declare any methods, they provide some ability to objects by relying on the internal mechanisms of the Java Virtual Machine (JVM). When an object implements a marker interface, the JVM recognizes this during runtime and provides the associated ability or behavior to that object.

Role of JVM (Why does the JVM provide this ability?)

The JVM is responsible for providing or enabling the abilities associated with marker interfaces. This is done to simplify programming and make Java language usage more straightforward for developers. By delegating certain responsibilities to the JVM, Java programs become more concise, easier to maintain, and less prone to errors.

Custom Marker Interfaces

Yes, we can create our own marker interface, but to do that, we would need to create our own JVM that supports all existing JVM features as well as any additional features we want to implement.

Adapter Class

An adapter class is a simple Java class that implements an java interface but provides empty implementations for all the methods in the interface.

Java
interface X {
    void m1();
    void m2();
    void m3();
    // ...
    void m1000();
}

abstract class AdapterX implements X {
    public void m1() {}
    public void m2() {}
    public void m3() {}
    // ...
    public void m1000() {}
}

When we implement an interface, we are required to provide implementations for every method in the interface, whether it’s required or not. This can increase the length of code unnecessarily.

Java
class Test implements X {
    public void m3() {
        // We only want to implement this method
    }

    // However, we need to implement all methods in the interface,
    // which increases the length of code unnecessarily
    public void m1() {}
    public void m2() {}
    public void m4() {}
    // ...
    public void m1000() {}
}

The problem with the above approach is that it increases the length of code and reduces readability. We can solve this problem by extending the Adapter class. This way, we only need to provide implementations for the required methods, and we are not responsible for implementing every method present in the interface, reducing the length of code.

Java
class Test extends AdapterX {
    public void m3() {
        // Provide implementation for only this method
    }
}

class Sample extends AdapterX {
    public void m7() {
        // Provide implementation for only this method
    }
}

class Demo extends AdapterX {
    public void m1000() {
        // Provide implementation for only this method
    }
}

The concept of marker interfaces and adapter classes simplifies the complexity of programming. They are powerful utilities for programmers, making their lives simpler.

Java Interface vs Abstract Class vs Concrete Class

Java Interface:

  • Definition: An interface in Java is a blueprint of a class that defines a set of abstract methods without providing any implementation.
  • Usage: Java Interfaces are used when we have a requirement specification without any knowledge of implementation details. For example, in Java servlets, the Servlet interface is used to define the methods that all servlets must implement.
  • Characteristics:
    • Contains only method declarations, without any implementation.
    • Provides a contract that concrete classes must adhere to.
    • Supports multiple inheritance.
    • Cannot contain constructors.

Abstract Class:

  • Definition: An abstract class in Java is a class that cannot be instantiated and may contain both abstract and concrete methods.
  • Usage: Abstract classes are used when we have some knowledge about implementation, but the implementation is not complete. They serve as a partial blueprint for concrete classes. For Example, GenericServlet and HttpServlet in Java servlets. These classes provide partial implementations of the Servlet interface, leaving some methods abstract for subclasses to implement.
  • Characteristics:
    • Can contain both abstract and concrete methods.
    • May include instance variables.
    • Cannot be instantiated directly, but can be subclassed.
    • Used to define common behavior for subclasses.

Concrete Class:

  • Definition: A concrete class in Java is a class that provides complete implementation for all its methods and can be instantiated directly.
  • Usage: Concrete classes are used when we have a complete understanding of implementation details and are ready to use them in the application.For Example, A custom servlet class (MyOwnServlet) that extends HttpServlet. This class provides complete implementations for all methods required by the servlet interface and can be directly instantiated for use in a web application.
  • Characteristics:
    • Provides complete implementation for all methods defined in its parent classes or interfaces.
    • Can be instantiated directly using the new keyword.
    • Can be subclassed further.
    • Typically used to create objects and perform specific tasks in the application.

Difference Between Interface and Abstract Class

Java Interface:

  1. If we lack knowledge about implementation and possess only the requirement specification, we opt for an interface.
  2. Inside an interface, every method is inherently public and abstract, whether explicitly declared or not. Hence, an interface is considered a 100% pure abstract class.
  3. Since every interface method is always public and abstract, we cannot declare them with the modifiers private, protected, final, static, synchronized, native, and strictfp.
  4. Every variable inside an interface is always public, static, and final, whether explicitly declared as such or not.
  5. As every interface variable is always public, static, and final, we cannot declare them with the modifiers private, protected, volatile, and transient.
  6. For interface variables, it is compulsory to perform initialization at the time of variable declaration; otherwise, a compilation error will occur.
  7. Inside an interface, we cannot declare static and instance blocks.
  8. Inside an interface, we cannot declare constructors.

Abstract class:

  1. If we discuss implementation but not completely (partial implementation), we should use an abstract class.
  2. Not every method inside an abstract class needs to be public and abstract; concrete methods are also allowed.
  3. There are no restrictions on the modifiers of methods in an abstract class.
  4. Not every variable inside an abstract class needs to be public, static, and final.
  5. There are no restrictions on the modifiers of variables in an abstract class.
  6. For variables in an abstract class, there’s no requirement to perform initialization at the time of declaration, so there are no restrictions on variable declaration.
  7. Inside an abstract class, we can declare static and instance blocks.
  8. Inside an abstract class, we can declare constructors.

Loopholes

As an abstract class cannot create an object and still contains a constructor, what is the need for a constructor in an abstract class?

The constructor in an abstract class will be executed whenever a child class object is created to perform initialization of child class objects only. Some properties inherited from the parent abstract class require initialization, necessitating the presence of an abstract class constructor. Its main advantage lies in writing less code and achieving code reusability. Although we cannot create an object directly or indirectly for an abstract class, we can still perform initialization of variables.

Anyway, we can’t create objects for abstract classes and interfaces. However, an abstract class can contain a constructor, whereas an interface does not. What is the reason for this?

The main purpose of a constructor is to perform initialization of instance variables. An abstract class can contain instance variables that are required for child objects, necessitating the presence of a constructor for the abstract class. However, every variable present inside an interface is always public static final, whether explicitly declared or not. Therefore, there is no chance of existing instance variables inside an interface, and the concept of a constructor is not required.

Whenever we create a child class object, the parent class object won’t be created; instead, the parent class constructor will be executed for the child object’s purpose only.

As an interface contains only abstract methods, and even in an abstract class, we can have only abstract methods, what is the difference between them? Can we replace an interface with an abstract class?

Yes, we can replace an interface with an abstract class, but it is not considered good programming practice. This is akin to recruiting an IAS officer for a sweeping activity. However, if everything is abstract, it is highly recommended to use an interface instead. Why?

Approach 1: Abstract Class
  1. Inheritance Constraint: While extending an abstract class, it’s not possible to extend any other class, limiting the inheritance concept.
  2. Object Creation Overhead: Object creation for classes extending an abstract class might be relatively more time-consuming due to the complexity of initialization logic.
Java
// Abstract class approach

abstract class X {
}

// 1) While extending an abstract class, we are unable to extend any other class, thus missing out on the inheritance concept.

class Test extends X {
}

// 2) In this case, object creation is costly.

Test t = new Test(); // Takes 2 minutes to create an object
Approach 2: Interface
  1. Inheritance Flexibility: Implementing an interface doesn’t restrict the ability to extend other classes, allowing more flexibility in inheritance.
  2. Object Creation Efficiency: Object creation for classes implementing an interface tends to be more efficient, as there is no overhead of initializing shared properties.
Java
// Interface approach

interface X {
}

// 1) While implementing an interface, we are able to extend some other class happily, thus not missing out on the inheritance concept.

class Test implements X {
}

// 2) In this case, object creation is not costly.

Test t = new Test(); // Takes 2 seconds to create an object

In short:

  • When using an abstract class, we cannot extend any other class while extending the abstract class itself, leading to a limitation in inheritance.
  • Object creation with abstract classes might be costly.
  • Conversely, with interfaces, we can happily implement them along with extending other classes, maintaining the inheritance concept intact.
  • Object creation with interfaces is typically not costly.

Conclusion

Java interfaces are a powerful feature of the Java programming language, allowing for abstraction, multiple inheritance, and polymorphism. By defining contracts that classes must adhere to, interfaces enable flexible and modular code design. Understanding how to define, implement, and use interfaces is essential for Java developers to write clean, maintainable, and extensible code.

In this guide, we’ve covered the basics of Java interfaces, including their definition, purpose, usage, best practices, and examples. With this knowledge, you should be well-equipped to leverage interfaces effectively in your Java projects.

Modifiers in java

Mastering Java Access Modifiers: A Comprehensive Guide for Enhanced Code Control

In the world of Java programming, understanding modifiers is crucial for writing efficient and secure code. Modifiers provide additional information about classes, methods, and variables, influencing their behavior and accessibility within the program. From controlling access levels to enforcing specific characteristics, modifiers play a vital role in shaping the structure and functionality of Java applications.

In this blog, we’ll delve into the various types of modifiers in Java, exploring their uses, syntax, and best practices. Whether you’re a beginner looking to grasp the basics or an experienced developer aiming to refine your skills, this comprehensive guide will serve as a valuable resource for mastering Java modifiers.

Class Level Modifiers

When writing our own class in Java, we need to provide some extra information to the JVM, such as whether the class is accessible everywhere, if child class creation is allowed, or whether object creation is possible or not, by using appropriate modifiers.

There are a total of 12 modifiers in Java:

  1. public
  2. private
  3. <default>
  4. protected
  5. final
  6. abstract
  7. static
  8. synchronized
  9. native
  10. strictfp
  11. transient
  12. volatile

Among them, there are 8 class-level modifiers:

For top-level classes, only five are allowed: public, <default>, final, abstract, and strictfp .

For inner classes, top-level modifiers along with private, protected, and static are permissible.

For Example:

Java
private class Test {
}

Results in a compilation error: “Modifier ‘private’ not allowed here.”

Java
public class Test {
  private class Test {
  }
}

Compiles without issues because private and static are allowed for inner classes.

Java
private class Test {
  // CE: modifier 'private' not allowed here.
}

public class Test {
  private class Test {
    // code compiler without any problem because 'private' and 'static' are allowed for inner classes.
  }

  static class Test {
    // code compiler without any problem because 'static' is allowed for inner classes.
  }
}

Similarly, this compiles fine since static is permitted for inner classes.

Access Specifiers and Modifiers in Java

In C and C++ languages, public, protected, <default>, and private are considered access specifiers, while all other modifiers are regarded as access modifiers. However, this distinction does not apply to Java, where all modifiers are considered as access modifiers.

  • public: Accessible everywhere within packages; even outside packages can access it.
  • <default>: Accessible everywhere within the current package; from outside the package, it cannot be accessed. Also known as package-level access.
  • protected: Enables access within the same package and subclasses, even if they are outside the package.
  • private: Restricts access to within the class where it’s declared, preventing access from other classes.
  • final modifiers: Applicable for classes, methods, and variables.
    • Final method: Method implementation is final, not allowed to change in a child class. Compiler Error: Child class cannot override overridden method declared as final in parent.Final class: If a class is final, it cannot be extended, meaning the whole implementation cannot be changed. Inheritance is not possible.Final variable: If a variable is final, its value cannot be changed, acting as a constant.
    Note: Every method in a final class is, by default, final implicitly, but not every variable in a final class needs to be final because non-final variables can be changed.

Advantages and Disadvantages of Final Modifiers:

Advantages of using final modifiers include enhanced security and ensuring a unique implementation of a class. However, there are disadvantages such as limitations on key Object-Oriented Programming (OOP) features like inheritance (due to final classes) and polymorphism (due to final methods). Thus, it’s generally not recommended to use the final keyword unless specific requirements necessitate its usage.

Before proceeding, a very silly but extremely important question regarding these concepts comes to my mind,

What are access specifiers and access modifiers in Java, and what is the difference between them?

In Java, access specifiers and access modifiers are used to control the visibility and accessibility of classes, methods, and variables within a program. These modifiers determine which parts of a program can access a particular member (class, method, or variable) and from where.

So,

Access Specifiers: Access specifiers define the scope of a class, method, or variable.

and,

Access Modifiers: Access modifiers are a subset of access specifiers that specifically modify the behavior of classes and members. Let’s take simple example, The final modifier can be applied to classes, methods, and variables. When applied to a class, it means that the class cannot be subclassed. When applied to a method, it means that the method cannot be overridden by subclasses. When applied to a variable, it means that the variable’s value cannot be changed once initialized.

Difference between Access Specifiers and Access Modifiers:

  • Access specifiers define the visibility of a class, method, or variable (e.g., public, protected, default, private).
  • Access modifiers are a subset of access specifiers and further modify the behavior of classes and members (e.g., final, abstract).

In short, accesAbstract Classs specifiers define where a class, method, or variable can be accessed from, while access modifiers modify the behavior of classes and members. However, it’s important to note that in Java, all of these are considered as access modifiers only.

Abstract Modifier in Java

In Java, the abstract modifier is applicable only to classes and methods, not to variables.

Abstract Method

Only declaration is available but no implementation is provided for that method. Hence, abstract methods end with a semicolon (;).

Example:

Java
public abstract int getNoOfWheels();
  • The child class is responsible for providing an implementation for the parent class’s abstract method.
  • If a class contains any abstract method, then it is compulsory for that class to be declared as abstract.
  • The main advantage of declaring abstract methods in a parent class is that every child class is compulsory to implement that abstract method.

It’s crucial to note that abstract methods are solely about declaration and lack implementation. Attempting to combine abstract with modifiers that imply implementation leads to a compilation error. These illegal combinations include final, native, synchronized, static, private, and strictfp. For instance:

Java
abstract final void m1()
// Results in a compilation error: "Illegal combination of modifiers 'abstract' and 'final'"

Means, abstract methods never deal with implementation. If any modifier pertains to implementation, then such modifier combinations with abstract are illegal. We get the same Compiler Error (CE).

Abstract Class

An abstract class is used for any Java class where object creation is not allowed due to partial implementation. Such types of classes are declared with abstract methods. Instantiation of an abstract class is not possible; we cannot create an object of an abstract class. If attempted, a Compiler Error (CE) is raised: “class is abstract, cannot be instantiated.”

If a class contains at least one abstract method, then it must be declared as abstract, otherwise, a Compiler Error will be encountered. Why? Because if a class contains at least one abstract method, its implementation is not complete, and it’s not recommended to create an object of such a class. To restrict object instantiation, it’s compulsory to declare the class as abstract. Even if a class contains zero abstract methods, we can still declare the class as abstract if we don’t want object creation of that class. For example, HttpServlet is abstract but doesn’t contain any abstract methods. Similarly, every adapter class is abstract but doesn’t contain any abstract methods.

When extending an abstract class, it’s necessary to implement each and every method present in the abstract class in the child class. If this is not possible, then the child class must be declared as abstract. In such cases, the next level child class is responsible for providing the implementation.

Abstract vs. Final

  • Abstract method: Compulsory to override in child class to provide implementation. Final method cannot be overridden; hence, the combination of final and abstract is illegal for methods.
  • For final classes, we cannot create a child class, whereas for abstract classes, we should create a child class to provide implementation. Hence, the combination of final and abstract is illegal for classes.
  • An abstract class can contain a final method, whereas a final class cannot contain an abstract method.

Example:

Java
abstract class Test {
    public final void m1() {
        // Implementation
    }
}

This is allowed, as an abstract class can contain final methods.

Java
final class Test {
    public abstract void m1(); // Not allowed
}

This is not allowed, as a final class cannot contain abstract methods.

It is highly recommended to use the abstract modifier in our day-to-day programming as it promotes several Object-Oriented Programming (OOP) features like polymorphism (method overriding) and inheritance (creating child classes). However, the final modifier suppresses OOP features, so it is not recommended to use it.

strictfp (strict floating point) Modifier

The strictfp modifier can be used with classes and methods but not for variables. Usually, floating-point results vary from platform to platform, and if we want platform-independent results, we use the strictfp modifier. A strictfp method needs to follow IEEE 754 standards to achieve platform-independent results. Since strictfp always pertains to implementation and abstract doesn’t, the combination of both modifiers is illegal.

strictfp at the Class Level

If every concrete method inside a class needs to follow IEEE 754 standards when using floating point, we declare that class as strictfp. However, if the class contains some abstract methods, then that class needs to be defined as abstract. Hence, the combination of strictfp and abstract is legal at the class level but illegal at the method level.

Member-Level Modifiers (Method or Variable Level Modifiers)

In Java, member-level modifiers are used to control access to methods or variables within classes. Here’s a breakdown of the commonly used member-level modifiers:

  1. public member: If a member is declared as public, it can be accessed from anywhere, but the corresponding class should be visible. Before checking member visibility, class visibility must be considered. This means that both the class and member need to be public for access from outside the package.
  2. default member: If a member is default, it can be accessed within the current package, but it cannot be accessed from outside the package. Hence, it is also known as package-level access.
  3. private member: If a member is private, it can be accessed only within the current class; outside the class, it cannot be accessed. Abstract methods are available to child classes for providing implementation, whereas private methods are not available to child classes for implementation. Therefore, the combination of abstract and private is illegal.
  4. protected member: If a member is protected, it can be accessed within the current package and also by child classes outside the package. It is one of the most misunderstood modifiers in Java. Protected access is equivalent to default access plus access for child classes.
    • We can access protected members within the current package anywhere using either parent reference or child reference. However, outside the package, we can access them only using child reference. If attempted to access using parent reference, a Compiler Error (CE) will be raised: “member has protected access in that class”.

Visibility rules for member-level modifiers

  • Within the same class: All access modifiers (private, <default>, protected, public) are allowed.
  • From child classes of the same package: <default>, protected, and public are allowed, but private is not.
  • From non-child classes of the same package: <default>, protected, and public are allowed, but private is not.
  • From child classes of outside packages: protected is accessible using child class references only, public is allowed, and others are not.
  • From non-child classes of outside packages: Only public access is allowed; others are not.

Final Variables

In Java, final variables are those whose values cannot be changed once they are assigned. Here’s a breakdown of final variables, let’s first focusing on final instance variables:

Final instance variable

The value of a variable that varies from object to object is called an instance variable or object-level variable. For every object, a separate instance variable copy is created. For instance variables, we don’t require explicit initialization because the JVM always provides default values. However, in the case of final instance variables, the JVM won’t provide a default value. We need to provide initialization explicitly whether it’s used or not; otherwise, we will encounter a Compiler Error (CE): “variable might not have been initialized”.

There are rules for initializing final instance variables:

Rule – Before constructor completion:

Case 1: At the time of declaration:

Java
class Test {
    final int x = 10;
}

Case 2: Inside an instance block:

Java
class Test {
    final int x;
    {
        x = 10;
    }
}

Case 3: Inside a constructor:

Java
class Test {
    final int x = 10;
    Test() {
        x = 10; // CE: Cannot assign value to final variable
    }
}

Note: If we try to initialize it in any method, we’ll get a CE: “cannot assign value to final variable”.

Final Static Variable

A final static variable is used when the value of the variable does not vary from object to object. Such a variable is called a static variable or class-level variable and is not recommended to be declared as an instance variable. It needs to be declared as static, and only a single copy exists at the class level, shared by every object.

In a static variable, the JVM always provides a default value at the time of initialization. We don’t need to do it explicitly. However, in the case of a final static variable, we must perform initialization explicitly; otherwise, we’ll encounter a Compiler Error (CE): “variable might not have been initialized”.

Rule: Before class loading completion, we need to perform initialization.

Case 1: At the time of declaration:

Java
class Test {
    final static int x = 10;
}

Case 2: Inside a static block:

Java
class Test {
    final static int x;
    static {
        x = 10;
    }
}

Note: We cannot initialize a final static variable inside a method; otherwise, we will get a CE: “cannot assign value to final variable”.

Final Local Variable

In Java, a final local variable is a variable declared within a method, block, or constructor, and its value remains constant once initialized. These variables are temporary and are stored on the stack, such variables are called local or temporary or stack or automatic variables.

Key points about final local variables:

  • JVM does not provide a default value for local variables. They must be explicitly initialized before use.
  • Even if a local variable is declared as final, initialization is only required if the variable is used. Unused final local variables do not need initialization.
  • The only applicable modifier for local variables is final. Attempting to use any other modifiers like public, private, protected, static, transient, or volatile will result in a compilation error.
  • Formal parameters of a method act as local variables within that method. They can also be declared as final, in which case, reassignment within the method is not allowed.

Example of declaring a final local variable:

Java
void myMethod() {
    final int x = 10; // Declaring and initializing a final local variable
    // Other code using variable x
}

Formal parameter example:

Java
void myMethod(final int param) {
    // param acts as a final local variable within this method
    // Reassignment of param is not allowed within this method
}

Note: The only applicable modifier for a local variable is final. If we try to use any other modifier like public, private, protected, static, transient, or volatile, we will encounter a Compiler Error (CE): “illegal start of expression”. If no modifier is declared, by default, it is <default>, but this rule applies only to instance and static variables, not for local variables.

Aso, formal parameters of a method simply act as local variables of that method. Formal parameters can be declared as final. If a formal parameter is declared as final, reassignment within the method is not permitted.

Static Modifiers

Static modifiers are applicable only for methods and variables and not for top-level classes. However, in static nested inner classes, it is applicable.

  • Instance Variable: A separate copy is created for every variable. In static variables, only one copy is created at the class level and shared by every object of the class.
  • Instance Variable vs. Static Variable: Instance variables can be accessed only from the instance area and cannot be accessed directly from the static area. However, static variables can be accessed from anywhere.

Consider the following declarations and let’s determine the correct combination:

I. int x = 10; 

II. static int x = 10; 

III. public void m1() { System.out.println(x); } 

IV. public static void m1() { System.out.println(x); }

  • Correct Combinations:
    • I and III: Correct. Instance variable x is accessed within an instance method. (Instance variable and instance method)
    • II and III: Correct. Static variable x is accessed within an instance method. (Static variable and instance method)
    • II and IV: Correct. Static variable x is accessed within an static method. (Static variable and static method)
  • Incorrect Combinations:
    • I and IV: Incorrect. Compiler Error (CE): “non-static variable x cannot be referenced from a static context.”
    • I and II: Incorrect. Duplicate variable x declaration within the same class.
    • III and IV: Incorrect. Duplicate method m1() declaration within the same class.

Regarding the main method overloading

If both public static void main(String[] args) and public static void main(int[] args) are present in the same class, and sop("String[]") is called, the output will be "String[]". This is because both methods are overloaded, and the class entry point is public static void main(String[] args).

Inheritance and Static Methods

The inheritance concept is applicable to static methods, including the main method. Hence, while executing a child class, if the child class doesn’t contain any main method, then the parent class’s main method will be executed.

Java
class P {
    public static void main(String[] args) {
        System.out.println("Parent main");
    }
}

class C extends P {
    public static void main(String[] args) {
        System.out.println("Child main");
    }
}

In this case, it is method hiding but not method overriding. So, when we run P.java, the output will be “Parent main”, and when we run C.java, the output will be “Child main”.

When to Use Static and Instance Methods:

  • Use instance methods when at least a single instance variable is being used.
  • Use static methods when no instance variable is being used, regardless of whether static variables are being used or not.

Abstract Static Combination:

The combination of abstract and static is illegal for methods because abstract means only declaration without implementation, while static means only implementation.

Synchronized Modifiers

In Java, the synchronized modifier is applicable only to methods and blocks but not to variables or classes.

Key points about the synchronized modifier:

  • It is used to prevent data inconsistency problems, such as race conditions, that may occur when multiple threads operate simultaneously on the same Java object.
  • When a method or block is declared as synchronized, only one thread is allowed to execute that method or block on the given object at a time, ensuring data consistency.
  • However, using synchronized can lead to increased waiting time for threads and performance issues. Therefore, it’s not recommended to use it unless there is a specific requirement to do so.

Combining synchronized with abstract is illegal for methods because:

  • abstract methods lack implementation and only provide a declaration.
  • synchronized methods require implementation to specify the synchronized behavior.
  • Combining these modifiers is contradictory, as abstract implies a lack of implementation while synchronized implies specific implementation details.

Therefore, synchronized abstract is not allowed for methods.

Native Modifiers

The native modifier is applicable only for methods and cannot be applied anywhere else.

Methods that are implemented in non-Java languages, mostly C and C++, are called native methods or foreign methods. Native methods are used when performance is critical. For example, the hashCode method in Java is implemented using native code.

The main objectives of using native methods are:

  1. To improve performance problems.
  2. To achieve machine-level communication.
  3. To utilize already existing legacy non-Java code.
Pseudo-code for using native methods in Java:

Load native libraries:

Java
static {
    System.loadLibrary("native_library_path");
}

Declare a native method:

Java
public native void m1();

Invoke a native method:

Java
n.m1();

For Java, native method implementations are already available in other old languages like C and C++. Therefore, we are not responsible for providing the implementation, and hence native methods end with a semicolon (;).

Java
public native void m1(); // CE: Native methods cannot have a body

The combination of native and abstract is illegal for methods because native methods already have implementation in old languages like C/C++, while abstract methods don’t have.

The combination of native and strictfp is also illegal for methods because strictfp follows IEEE 754 standards, but there is no guarantee that native methods, which are implemented in C/C++, adhere to IEEE 754 standards.

The main advantage of using native methods is improved performance, but the main disadvantage is that it introduces Java dependency as we completely depend on other language implementations.

Transient Modifiers

In Java, the transient modifier is applicable only to variables and is commonly used in the context of serialization.

Key points about the transient modifier:

  • It is used to indicate that a variable should not be serialized.
  • When an object is serialized, all of its non-transient instance variables are saved to a file or sent over a network. However, transient variables are excluded from this process.
  • Transient variables are useful when there are sensitive or unnecessary fields that should not be saved during serialization. For example, fields containing passwords or temporary data.
  • When an object is deserialized, transient variables will be initialized to their default values (e.g., null for objects, 0 for numeric types).
  • Using transient achieves better security by preventing certain data from being persisted or transmitted.
  • Transient variables are typically declared as private, though this modifier is not strictly necessary for serialization purposes.

Example usage:

Java
import java.io.Serializable;

class MyClass implements Serializable {
    private transient String sensitiveData; // This variable will not be serialized

    // Other non-transient variables and methods
}

In this example, the sensitiveData variable will not be included in the serialization process, ensuring that sensitive information is not persisted or transmitted.

Volatile Modifiers

The volatile modifier is applicable only for variables and not for anywhere else.

If the value of a variable keeps changing in multiple threads, there may be a chance of a data inconsistency problem. We can solve this problem using the volatile keyword.

When a variable is declared as volatile, JVM will create a separate copy of that variable for each thread. Any modification performed by any thread is on its own copy, ensuring that it will not affect the value seen by any other thread.

The combination of final and volatile is illegal for variables because final implies that the value never changes, while volatile implies that the value keeps on changing.

Advantage: Overcoming data inconsistency problems.

Disadvantage: Creating and maintaining separate copies of variables, leading to reduced performance and increased memory usage. Therefore, volatile is deprecated and not recommended for use.

Short Modifiers Summary:

Here’s a brief revision of the modifiers mentioned:

  • Local Variable: Only applicable modifier is final.
  • Constructor: Applicable modifiers are public, private, protected, and <default>.
  • Methods: Only applicable modifier is native.
  • Variables: Applicable modifiers are volatile and transient.
  • Class (not for interface): Only applicable modifier is final.
  • Class (not for enum): Applicable modifiers are final and abstract.

Conclusion

Java modifiers serve as powerful tools for developers to enhance the functionality, security, and performance of their applications. By understanding the nuances of each modifier type and applying them appropriately, programmers can write more robust and maintainable code.

From controlling access levels with keywords like public, private, protected, and <default>, to managing synchronization and serialization with volatile and transient, modifiers offer a wide range of capabilities to meet various programming needs.

As you continue your journey in Java development, remember to leverage modifiers effectively, striking the right balance between accessibility, security, and performance. With practice and a solid understanding of modifiers, you’ll be well-equipped to tackle complex programming challenges and build successful Java applications.

Java Class Structure and Import Statements

Java Class Structure and Import Statements: A Comprehensive Guide for Developers

Java programming offers a flexible structure for organizing classes and utilizing import statements efficiently. Let’s delve into the various aspects of class organization, naming conventions, and import statements in Java.

Java Class Organization and Naming Conventions

Any Java class can contain any number of Java classes, and we can give any name to it, whether it is the containing class name or any other user-defined name. For example:

Java
class A {
  // Class A implementation
}

class B {
  // Class B implementation
}

class C {
  // Class C implementation
}

Case 1: Here we can take any name like A.java, B.java, C.java, or amol.java, so there are no restrictions.

Case 2: If we declare a class with the public modifier, then the class name should match that of the file. For instance, in the above example, if public class B is declared, then the class name should be B.java; otherwise, we will get a compilation error: “class B is public, should be declared file name as B.java.”

Case 3: If a class contains multiple public classes, then at most one public class name should match the file name. For example, if public class B and public class C are present in the above code, then the file name should be C.java. Otherwise, we will get a compilation error: “class C is public, declare file name as C.java.”

Execution Behavior

In Java, a single source file can accommodate multiple classes, each with its main method. For example:

Java
class A {
  public static void main(String args[]) {
    System.out.println("A class main");
  }
}

class B {
  public static void main(String args[]) {
    System.out.println("B class main");
  }
}

class C {
  public static void main(String args[]) {
    System.out.println("C class main");
  }
}

class D {
}

When running Java programs with multiple classes, the behavior varies based on the class invoked:

If the above class is named “amol.java,” it compiles with that name, but different source files are generated for each class name, such as A.java, B.java, C.java, and D.java. However, when we run with different class names, we get different output:

java A –> output: A class main

java B –> output: B class main

java C –> output: C class main

java D –> Runtime Error: NoSuchMethodError exception

java amol –> Runtime Error: NoClassDefFoundError

It is highly recommended to have only one class per source file and name the program the same as the class name. This practice improves readability and maintainability. This practice also aids in avoiding issues like the one encountered with java D. Additionally, importing statements in the class should be written so that there is no need to write the fully qualified name.

Import Statements

Import statements in Java facilitate the use of classes and packages within the code. There are two types of import statements:

  1. Explicit Import: Imports a specific class or interface -> import java.util.ArrayList;
  2. Implicit Import: Imports all classes within a package -> import java.util.*;

Resolving Class Names

While resolving class names, the compiler always gives precedence in the following manner:

  1. Explicit class import take precedence
  2. Classes present in the current working directory (default package) are considered next.
  3. Implicit class import are checked last.

We don’t require import statements for the java.lang package and the default package (CWD – current working directory).

Static Import Statements

Import statements are a concept at the compiler level and have no effect on execution time.

A key difference between the #include statement in C and the import statement in Java is their behavior:

  • The #include statement in C is a static include type, loading all required classes at the beginning, which can lead to unnecessary memory usage and reduced performance.
  • The import statement in Java is a dynamic include type, loading classes on demand or “on the fly,” reducing memory usage and improving performance.

According to Sun Microsystems, static import statements reduce code length and enhance code readability. However, some programmers argue that it decreases readability and increases complexity.

In Java, without static imports, we can use class names with static methods like Math.sqrt() and Math.random(). With static imports, we can directly use methods without the class name, by using the import static statement. For instance, import static java.lang.Math.sqrt; allows us to use sqrt(10) directly without the class name.

Java
import static java.lang.Math.sqrt;

public class Example {
  public static void main(String[] args) {
    System.out.println(sqrt(10));
  }
}

Static import statements provide a way to access static members of a class directly without qualifying the class name. This can enhance code readability but may increase complexity.

System.out.println()

The System.out.println() method in Java can be dissected as follows:

  • System: This is a class present in the java.lang package.
  • out: It’s a static variable within the System class, of the type PrintStream.
  • println(): This is a method present in the PrintStream class.

While resolving static members, in the case of static statements, the compiler will always consider the following order: 

  1. Current class static members 
  2. Explicit static import 
  3. Implicit static import. 

Also,

There are two types of static import:

  1. Explicit static import
  2. Implicit static import.

Java Package

In Java, a package is a group of interrelated classes and interfaces bundled into a single unit. The structure of a Java source file typically includes:

  • Package statement: This appears at most once and defines the package of the file’s contents.
  • Import statements: Any number of import statements can be included to import classes from other packages.
  • Class, interface, or enum declarations: There can be any number of these declarations within the source file.

Conclusion

Understanding Java class structure and import statements is crucial for writing efficient and maintainable code. By organizing classes logically and utilizing import statements effectively, developers can improve code readability and performance in Java applications.

sorting algorithms in Java

A Comprehensive Guide to Sorting Algorithms in Java

Sorting algorithms are fundamental tools in computer science used to organize data efficiently. Sorting algorithms are techniques used to rearrange elements in a list or array in a specific order. The order could be ascending or descending based on certain criteria, such as numerical value or alphabetical order. Sorting algorithms play a crucial role in various applications, including databases, search algorithms, and more.

In Java, sorting algorithms can be implemented using various approaches, each with its unique characteristics and performance implications. In Java, there are various sorting algorithms, each with its advantages and disadvantages. In this guide, we’ll explore some of the most commonly used sorting algorithms, including their implementations, complexities, and use cases.

Sorting algorithms in Java : Insertion Sort

Insertion Sort is a simple sorting algorithm that builds the final sorted array one element at a time. It iterates through the input array, removing one element per iteration and then finding its correct position in the sorted array.

Java
public int[] insertionSort(int[] list) {
   
  int i;   // Counter for outer loop
  int j;   // Counter for inner loop

  int key;    // To hold the key value purpose we use it 

  int temp;   // It will be helpful while swapping values 

  for(i = 1; i < list.length; i++) {
     // Outer for loop starts with 1 and checks till the end value.
     // Here we consider the 0th element is already sorted that's why we start from 1
  
     key = list[i];   // It is our key value which is list[i] for every iteration  
     j = i - 1;         // It will start point of comparison moving downwards till 0 
   
     // We end the while loop when j value reaches 0 or when the key value is no longer smaller than the value
     while(j >= 0 && key < list[j]) {
       // Swap the values using the temp variable and arrange them in increasing order        
       temp = list[j];
       list[j] = list[j + 1];
       list[j + 1] = temp;
       j--;     // Decreasing j by 1 as we move towards the left 
     }

   }
   
  return list;
}

This insertionSort method implements the insertion sort algorithm for sorting an array of integers in increasing order. It iterates through the array, considering each element as a key and inserting it into the correct position in the sorted subarray to its left. The inner loop compares the key with the elements to its left and swaps them if necessary to maintain the sorted order. Finally, it returns the sorted array.

Explanation

Given an array of integers, sort them in increasing order using insertion sort:

Initial array: 5, 8, 1, 3, 9, 6

Definition of sorted: All items to the left are smaller. For example, 5 is already considered sorted as there are no elements to its left.

So, we start our insertion sort process from index 1:

  1. Key (k) = 8. Compare k < 5? No. So, 8’s position is correct. Consider 5 and 8 as sorted.
  2. Next, at index 2:
    • Key (k) = 1.
    • Compare k < 8? Yes, swap: 5, 1, 8, 3, 9, 6.
    • Compare k < 5? Yes, swap: 1, 5, 8, 3, 9, 6.
    • Element till index 2 is sorted.
  3. Start at index 3:
    • Key (k) = 3.
    • Compare k < 8? Yes, swap: 1, 5, 3, 8, 9, 6.
    • Compare k < 5? Yes, swap: 1, 3, 5, 8, 9, 6.
    • Compare k < 1? No, no swap.
    • Elements till index 3 are now sorted.
  4. Start at index 4:
    • Key (k) = 9.
    • Compare k < 8? No, no swap. Also, all elements left of 8 are sorted, indicating 9’s position is correct.
    • Elements till index 4 are also sorted.
  5. Start at index 5:
    • Key (k) = 6.
    • Compare k < 9? Yes, swap: 1, 3, 5, 8, 6, 9.
    • Compare k < 8? Yes, swap: 1, 3, 5, 6, 8, 9.
    • Compare k < 5? No, no swap. End of loop.

Finally, we obtain the sorted list using insertion sort. Final array: {1, 3, 5, 6, 8, 9}

Note: Insertion Sort is not a fast sorting algorithm as it uses nested loops to shift items into place. It is useful only for small datasets. It runs in O(n^2) time complexity and O(1) space complexity.

Selection Sort

Selection Sort is a simple sorting algorithm that repeatedly selects the smallest (or largest) element from the unsorted part of the array and moves it to the beginning. This process continues until the entire array is sorted.

Java
public int[] selectionSort(int[] list) {
    int i; // Outer loop counter
    int j; // Inner loop counter
    int minValue; // To track minimum value
    int minIndex; // To track the index of the minimum value in the array
    int temp = 0; // For swapping purposes

    // Let's consider this array: 7, 8, 5, 4, 9, 2

    // Outer loop should iterate from 0 to 5 (i.e., 6 iterations)
    // So the outer loop should be: for i = 0 to 5
    for (i = 0; i < list.length; i++) {
        // Initialize minValue and minIndex to the first unsorted item each time through the outer loop
        minValue = list[i];
        minIndex = i;

        // Inner loop starts at the first unsorted item and continues through the last item in the list
        // So the inner loop should be: for j = i to 5
        for (j = i; j < list.length; j++) {
            // Inside the inner loop, if the list item is less than the current minValue,
            // update minValue and store its index in minIndex
            if (list[j] < minValue) {
                minValue = list[j];
                minIndex = j;
            }
        }

        // After that, we check if the minValue is less than the value at the current index of the outer loop
        // If yes, then swap that value with the value at minIndex
        if (minValue < list[i]) {
            temp = list[i];
            list[i] = list[minIndex];
            list[minIndex] = temp;
        }
    }
    return list;
}

This selectionSort method implements the selection sort algorithm for sorting an array of integers in increasing order. It iterates through the array, finding the minimum element in the unsorted part of the array and swapping it with the first unsorted element. This process repeats until the entire array is sorted. Finally, it returns the sorted array.

Explanation

Selection Sort: It finds the smallest item from the unsorted part of the list and moves it to the first element of the list while performing selection sort.

Consider this array: 7, 8, 5, 4, 9, 2 // also imagine indices from 0 to 5.

So initially, our minValue is 7 and minIndex is 0.

  • Now, comparison:
    • Is 8 < minValue (7)? No.
    • Is 5 < minValue (7)? Yes, the new minValue is 5 and minIndex is 2.
    • Is 4 < minValue (5)? Yes, the new minValue is 4 and minIndex is 3.
    • Is 9 < minValue (4)? No. Is 2 < minValue (4)? Yes, the new minValue is 2 and minIndex is 5.

We found our first smallest element, so we swap that element with the first element ==> 2, 8, 5, 4, 9, 7.

  • Our second iteration starts with index 1, so our first element is 8, and minValue is 8, with minIndex as 1. Now, start comparison toward the right side of 8.
    • Is 5 < minValue (8)? Yes, the new minValue is 5, and minIndex is 2.
    • Is 4 < minValue (5)? Yes, the new minValue is 4, and minIndex is 3.
    • Is 9 < minValue (4)? No.
    • Is 7 < minValue (4)? No.

We found our smallest element for the second iteration, so we swap it with the first element ==> 2, 4, 5, 8, 9, 7.

  • Now, start comparison towards the right side of 5, and we see that 5 is the smallest element among them, so there’s no need to swap.
  • Again, we start comparison towards the right side of 8. So, minValue is 8, and minIndex is 3.
    • Is 9 < minValue (8)? No.
    • Is 7 < minValue (8)? Yes, the new minValue is 7, and minIndex is 5.

So, swap ==> 2, 4, 5, 7, 9, 8.

  • Now, for the next iteration, minValue is 9, and minIndex is 4.
    • Is 8 < minValue (9)? Yes, the new minValue is 8, and minIndex is 5.

So, swap ==> 2, 4, 5, 7, 8, 9.

Note: Selection Sort is not a fast sorting algorithm because it uses nested loops to sort. It is useful only for small datasets means suitable for small arrays or when auxiliary memory is limited. It runs in O(n^2) time complexity and O(1) space complexity.

Bubble Sort

Bubble Sort is another simple sorting algorithm that repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. The pass through the list is repeated until the list is sorted.

Java
public int[] bubbleSort(int[] list) {
    int i; // outer loop counter
    int j; // inner loop counter
    int temp = 0; // for swapping purpose

    for (i = 0; i < list.length - 1; i++) { // outer loop iterate till the end
        for (j = 0; j < list.length - 1 - i; j++) { // inner loop iterate till the end where already sorted element bubbled at the end
            if (list[j] > list[j + 1]) { // if we consider two adjacent values then previous value greater means we need to swap them with next value
                temp = list[j];
                list[j] = list[j + 1];
                list[j + 1] = temp;
            }
        }
    }
    return list;
}

This bubbleSort method implements the bubble sort algorithm for sorting an array of integers in increasing order. It iterates through the array multiple times, comparing adjacent elements and swapping them if they are in the wrong order. This process repeats until the array is sorted. Finally, it returns the sorted array.

Explanation

Bubble Sort: It is a simple sorting algorithm that repeatedly compares adjacent elements in the list and swaps them if they are in the wrong order. This process continues until the list is sorted.

Unsorted list: 5, 8, 1, 6, 9, 2 // Imagine indices from 0 to 5.

So, what is bubble sort?

In bubble sort, we repeatedly compare adjacent items, for example, 5 and 8, so that with every iteration, larger items “bubble” to the right side.

  • First iteration:
    • Is 5 > 8? No.
      • Is 8 > 1? Yes ==> Swap ==> 5, 1, 8, 6, 9, 2
        • Is 8 > 6? Yes ==> Swap ==> 5, 1, 6, 8, 9, 2
          • Is 8 > 9? No.
            • Is 9 > 2? Yes ==> Swap ==> 5, 1, 6, 8, 2, 9
  • Second iteration:
    • Is 5 > 1? Yes ==> Swap ==> 1, 5, 6, 8, 2, 9
      • Is 5 > 6? No.
        • Is 6 > 8? No.
          • Is 8 > 2? Yes ==> Swap ==> 1, 5, 6, 2, 8, 9
  • Third iteration:
    • Is 1 > 5? No.
      • Is 5 > 6? No.
        • Is 6 > 2? Yes ==> Swap ==> 1, 5, 2, 6, 8, 9
          • Is 6 > 8? No.
            • Is 8 > 9? No.
  • Fourth Iteration:
    • Is 1 > 5? No.
      • Is 5 > 2? Yes ==> Swap ==> 1, 2, 5, 6, 8, 9
        • Is 5 > 6? No.
          • Is 6 > 8? No.
            • Is 8 > 9? No.
  • Fifth Iteration:
    • All elements are in sorted order; no need to do any swapping.

Final sorted list: 1, 2, 5, 6, 8, 9

Note: Bubble Sort is not an efficient sorting algorithm because it uses nested loops. It is useful only for small data sets. It runs in O(n^2) and O(1) space complexity.

Merge Sort

Merge Sort is a divide-and-conquer algorithm that divides the input array into two halves, sorts each half separately, and then merges the sorted halves.

Java
// passing low and high index to this function
// e.g 38,27,43,3,9,82,10 ==> low -> 0 and high -> 6 (size - 1)th index
private void mergeSort(int low, int high) {
    if (low < high)      // base condition
    {
        int mid = low + (high - low) / 2;    // we need to find mid (it is 3 here)
        // perform recursive division into low to mid and then mid + 1 to high
        mergeSort(low, mid);      // 38,27,43,3  ===for next recursive call ===> 38,27 and 43,3 === next recursive call ==> 38 and 27 | 43 and 3 here as we reached base condition where low and high are equal and return back and execute remaining code
        mergeSort(mid + 1, high);   // we start when the above statement executed completely and reached the base case, so now 9,82,10  === for the next recursive call ===> 9,82 and 10  ==== for the next call ===> 9 and 82 | 10 here also we reached the base condition so go back now
        merge(low, mid, high);    // it will just merge the elements according to the sorting order
    }
}

// we pass low, mid, and high value
private void merge(int low, int mid, int high) {
    // for copying purposes means we copy number values to helper[] from low to till high
    for (int i = low; i <= high; i++) {
        helper[i] = numbers[i];
    }

    int i = low;
    int j = mid + 1;
    int k = low;

    // we tried to sort the numbers[] element and placed them in exact same order
    while (i <= mid && j <= high) {
        if (helper[i] <= helper[j]) {
            numbers[k] = helper[i];
            i++;
        } else {
            numbers[k] = helper[j];
            j++;
        }
        k++;
    }

    // if still some values remain to be placed in sorted order in numbers[] we will do that here
    while (i <= mid) {
        numbers[k] = helper[i];
        k++;
        i++;
    }
}

These methods implement the merge sort algorithm. mergeSort is a recursive method that divides the array into two halves and recursively calls itself on each half until it reaches the base case where low is no longer less than high. Then, the merge method merges the two sorted halves into a single sorted array.

Explanation

Merge Sort is a divide and conquer algorithm. It divides the input array into two halves, recursively calls itself for the two halves, and then merges the two sorted halves.

For example, with an input array like 38, 27, 43, 3, 9, 82, 10:

First, we call mergeSort(0, 6) as the initial entry point.

In mergeSort:

  • We find the mid index (low + (high – low) / 2) to split the array into two halves.
  • Then, we recursively call mergeSort for the left half (low to mid) and the right half (mid + 1 to high).
  • Finally, we merge the sorted halves using the merge function.

In merge:

  • We create a helper array to copy elements from the original array.
  • We iterate through the two halves, comparing elements and merging them into the original array in sorted order.
  • If any elements remain in the left half after merging, we copy them back to the original array.

Let’s go through the merge sort algorithm step by step:

  1. Initial array: {38, 27, 43, 3, 9, 82, 10}
  2. Initial call to mergeSort(0, 6):
    • Since low (0) is less than high (6), we proceed.
    • Calculate mid = 0 + (6 – 0) / 2 = 3
  3. Recursive call 1: mergeSort(0, 3)
    • Since low (0) is less than high (3), we proceed.
    • Calculate mid = 0 + (3 – 0) / 2 = 1
  4. Recursive call 2a: mergeSort(0, 1)
    • Since low (0) is less than high (1), we proceed.
    • Calculate mid = 0 + (1 – 0) / 2 = 0
  5. Recursive call 2b: mergeSort(1, 3)
    • Since low (1) is less than high (3), we proceed.
    • Calculate mid = 1 + (3 – 1) / 2 = 2
  6. Recursive call 3: mergeSort(1, 2)
    • Since low (1) is less than high (2), we proceed.
    • Calculate mid = 1 + (2 – 1) / 2 = 1
  7. Recursive call 4: mergeSort(2, 3)
    • Since low (2) is less than high (3), we proceed.
    • Calculate mid = 2 + (3 – 2) / 2 = 2
  8. Back to Recursive call 4: merge(1, 1, 2)
    • Helper array: {27, 43, 43, 3, 9, 82, 10}
    • i = 1, j = 2, k = 1
    • Compare helper[i] (27) with helper[j] (43), place the smaller one (27) in numbers[k] (numbers[1]), increment i and k.
    • numbers: {38, 27, 43, 3, 9, 82, 10}
    • Increment k to 2 and i to 2.
  9. Back to Recursive call 3: merge(1, 1, 3)
    • Helper array: {27, 38, 43, 3, 9, 82, 10}
    • i = 1, j = 3, k = 1
    • Compare helper[i] (27) with helper[j] (3), place the smaller one (3) in numbers[k] (numbers[1]), increment j and k.
    • numbers: {38, 3, 43, 3, 9, 82, 10}
    • Increment k to 2 and j to 4.
    • Compare helper[i] (27) with helper[j] (9), place the smaller one (9) in numbers[k] (numbers[2]), increment j and k.
    • numbers: {38, 3, 9, 3, 9, 82, 10}
    • Increment k to 3 and j to 5.
    • Compare helper[i] (27) with helper[j] (82), place the smaller one (27) in numbers[k] (numbers[3]), increment i and k.
    • numbers: {38, 3, 9, 27, 9, 82, 10}
    • Increment k to 4 and i to 2.
  10. Back to Recursive call 2a: merge(0, 0, 1)
    • Helper array: {38, 38, 9, 27, 9, 82, 10}
    • i = 0, j = 1, k = 0
    • Compare helper[i] (38) with helper[j] (3), place the smaller one (3) in numbers[k] (numbers[0]), increment j and k.
    • numbers: {3, 3, 9, 27, 9, 82, 10}
    • Increment k to 1 and j to 2.
    • Compare helper[i] (38) with helper[j] (9), place the smaller one (9) in numbers[k] (numbers[1]), increment j and k.
    • numbers: {3, 9, 9, 27, 9, 82, 10}
    • Increment k to 2 and j to 3.
    • Compare helper[i] (38) with helper[j] (27), place the smaller one (27) in numbers[k] (numbers[2]), increment i and k.
    • numbers: {3, 9, 27, 27, 9, 82, 10}
    • Increment k to 3 and i to 1.
  11. Back to Recursive call 1: merge(0, 1, 3)
    • Helper array: {3, 9, 27, 38, 9, 82, 10}
    • i = 0, j = 2, k = 0
    • Compare helper[i] (3) with helper[j] (9), place the smaller one (3) in numbers[k] (numbers[0]), increment i and k.
    • numbers: {3, 9, 27, 38, 9, 82, 10}
    • Increment k to 1 and i to 1.
    • Compare helper[i] (9) with helper[j] (27), place the smaller one (9) in numbers[k] (numbers[1]), increment i and k.
    • numbers: {3, 9, 27, 38, 9, 82, 10}
    • Increment k to 2 and i to 2.
    • Compare helper[i] (27) with helper[j] (38), place the smaller one (27) in numbers[k] (numbers[2]), increment j and k.
    • numbers: {3, 9, 27, 38, 9, 82, 10}
    • Increment k to 3 and j to 3.
  12. Final array: {3, 9, 27, 38, 9, 82, 10}

This process continues until the entire array is sorted. The merge sort algorithm effectively divides the array into smaller segments, sorts them individually, and then merges them back together in sorted order.

Note: Merge Sort has a time complexity of O(n log n) in the average and worst cases, and O(n) in the best case, making it efficient for large datasets and O(n) space complexity.

Quick Sort

Quick Sort is another divide-and-conquer algorithm that picks an element as a pivot and partitions the array around the pivot. It then recursively sorts the subarrays.

Java
private void quickSort(int low, int high) {
    int i = low, j = high;
    int pivot = numbers[low + (high - low) / 2];  // Choosing the pivot element

    // Partitioning the array
    while (i <= j) {
        // Finding element on the left side that should be on the right
        while (numbers[i] < pivot) {
            i++;
        }
        // Finding element on the right side that should be on the left
        while (numbers[j] > pivot) {
            j--;
        }

        // If we found a value on the left that should be on the right and vice versa, swap them
        if (i <= j) {
            exchange(i, j);
            i++;
            j--;
        }
    }

    // Recursively sort the left and right partitions
    if (low < j) {
        quickSort(low, j);
    }
    if (i < high) {
        quickSort(i, high);
    }
}

This quickSort method implements the Quick Sort algorithm. It chooses a pivot element (here, the middle element) and partitions the array into two sub-arrays such that elements less than the pivot are on the left, and elements greater than the pivot are on the right. It then recursively sorts the sub-arrays until the entire array is sorted. The exchange method is assumed to swap the values at two indices in the array.

Explanation

Quick Sort is a divide and conquer algorithm. In this algorithm, the original data is divided into two parts, sorted individually, and then combined.

suppose

{6,4,2,1,5,3} is an array where low is pointing to 0th index and high is pointing to last index.

means,

  • Set low to 0 and high to array.length - 1, then pass these values to quickSort(low, high).
  • Assign low to i, which is the left side counter of the pivot, and assign high to j, which is the right side counter of the pivot.
  • Find the pivot. What is a pivot? A pivot is a point we consider while sorting. We arrange smaller values than the pivot to its left side and greater values to its right side. We can take any value as the pivot, but it’s best to take it randomly. Here, we use the middle element as the pivot.
  • We will check i <= j. If low becomes greater than high, we stop our following checks: i) First, we check the left side of the pivot to find any greater value than the pivot. If we find such a value, we terminate the while loop. Otherwise, we iterate until we reach the pivot. Since we don’t have any problem with values smaller than the pivot (as those should be on the left side), we increment i and move towards the pivot to check the next value. ii) After completing the left side (either by complete check or finding a greater value, terminating the while loop), we look at the right side of the pivot. We apply the same logic: while(any value > pivot). If true, we decrement j. If we find any smaller value than the pivot, the while loop terminates. iii) So, in the above two cases, we find whether the left side of the pivot will contain any greater value than the pivot or whether the right side of the pivot will contain any smaller value than the pivot. Because we sort the values according to the pivot, if we find a smaller value to the right and a larger value to the left, we need to place them on their respective sides. For this purpose, we use another if() condition: if i is not greater than j, then only do we exchange the values. Finally, we increment i and decrement j.
  • As we perform sorting according to the pivot, only small values are placed on the left side of the pivot and large values are placed on the right side. However, we notice that neither the left side nor the right side is sorted.

For example, with an array like 6, 4, 2, 1, 5, 3:

  • We call quickSort(0, 5) initially, where low = 0 and high = 5.
  • We select a pivot element, here we choose the middle element, which is 2.
  • We partition the array around the pivot, ensuring that elements less than the pivot are on the left and elements greater than the pivot are on the right.
  • We recursively call quickSort for the left and right partitions, ensuring that all elements are sorted individually.
  • We repeat this process until the entire array is sorted.

Let’s go through the Quick Sort algorithm step by step:

  1. Initial array: {6, 4, 2, 1, 5, 3}
  2. Initial call to quickSort(0, 5):
    • i = 0, j = 5
    • Pivot = numbers[0 + (5 – 0) / 2] = numbers[2] = 2
  3. While loop (i=0, j=5):
    • i moves from left to right until it finds a value greater than or equal to the pivot. (i stops at 4)
    • j moves from right to left until it finds a value less than or equal to the pivot. (j stops at 3)
    • Since i <= j, exchange(numbers[i], numbers[j]) is performed.
    • After exchange, array becomes {3, 4, 2, 1, 5, 6}
    • Increment i and decrement j (i = 1, j = 4)
  4. While loop (i=1, j=4):
    • i moves until it finds a value greater than or equal to the pivot. (i stops at 4)
    • j moves until it finds a value less than or equal to the pivot. (j stops at 1)
    • Since i <= j, exchange(numbers[i], numbers[j]) is performed.
    • After exchange, array becomes {3, 1, 2, 4, 5, 6}
    • Increment i and decrement j (i = 2, j = 3)
  5. While loop (i=2, j=3):
    • i moves until it finds a value greater than or equal to the pivot. (i stops at 2)
    • j moves until it finds a value less than or equal to the pivot. (j stops at 2)
    • Since i <= j, exchange(numbers[i], numbers[j]) is performed.
    • After exchange, array remains {3, 1, 2, 4, 5, 6}
    • Increment i and decrement j (i = 3, j = 1)
  6. While loop termination (i=3, j=1):
    • i > j, so the while loop terminates.
  7. Recursive call 1: quickSort(0, 1)
    • This is the left side partition.
    • i = 0, j = 1
    • Subarray: {3, 1}
    • Pivot = numbers[0] = 3
    • While loop terminates immediately as i > j.
  8. Recursive call 2: quickSort(3, 5)
    • This is the right side partition.
    • i = 3, j = 5
    • Subarray: {4, 5, 6}
    • Pivot = numbers[3] = 4
    • While loop terminates immediately as i > j.
  9. Final sorted array: {1, 2, 3, 4, 5, 6}

This process continues recursively until all partitions have size 1 or less, meaning the array is sorted. The Quick Sort algorithm effectively divides the array into smaller partitions, sorts them individually, and then combines them to get the sorted array.

Note: Quick Sort has a time complexity of O(n log n) in the average and best cases, and O(n^2) in the worst case. However, its average case performance is typically much better than other O(n^2) algorithms like Bubble Sort and Selection Sort. It has space complexity O(log n) to O(n). Also, Quick Sort is efficient and widely used, but it has a higher worst-case time complexity compared to Merge Sort.

Conclusion

In this guide, we’ve covered several sorting algorithms in Java, including Selection Sort, Bubble Sort, Insertion Sort, Merge Sort, and Quick Sort. Each algorithm has its advantages and disadvantages, and the choice of algorithm depends on factors such as the size of the dataset, memory constraints, and desired performance characteristics. By understanding these sorting algorithms, you can choose the most appropriate one for your specific use case and optimize the efficiency of your Java applications.

Graphs in java

Mastering Graphs in Java: A Comprehensive Guide for Developers

Graphs are fundamental data structures in computer science and are widely used to model complex relationships between entities. They are versatile and can represent a variety of real-world scenarios such as social networks, transportation networks, and computer networks. In this blog post, we’ll delve into some essential graph algorithms implemented in Java, covering Depth-First Search (DFS), Breadth-First Search (BFS), Dijkstra’s Shortest Path Algorithm, detecting cycles in a directed graph, and performing a topological sort.

Understanding Graphs

Before diving into the algorithms, let’s briefly review what graphs are. We know that data arranged in a linear manner is called linear data structures, such as Arrays, LinkedLists, Stacks, and Queues. Whereas, when data is stored non-linearly, it is referred to as non-linear data structures, such as Trees and Graphs. A graph consists of a set of vertices (nodes) and a set of edges (connections) that establish relationships between these vertices. Edges can be directed or undirected, depending on whether they have a specific direction or not. Graphs can also be weighted, where each edge has an associated weight or cost.

A graph is a powerful non-linear data structure consisting of two components:

  • Vertices (nodes): Represent individual entities or data points. Think of them as points on a map.
  • Edges: Represent connections between vertices, indicating relationships or interactions. Edges can be bidirectional (undirected) or unidirectional (directed), like arrows showing one-way relationships.

A graph G is an ordered pair consisting of a set V of vertices and a set E of edges.

G = (V,E)

In an ordered pair, (a,b) is not equivalent to (b,a) if a ≠ b.

In an unordered pair, {a,b} equals {b,a}.

Edges:

  • Directed: (u,v) i.e., u → v
  • Undirected: {u,v} i.e., u — v

Example:

V = {v1, v2, v3, v4, v5, v6, v7, v8} // Set of vertices

E = { {v1, v2}, {v1, v3}, {v2, v4}, {v3, v5}, {v4, v6}, {v5, v7}, {v6, v8}, {v7, v8}, {v2, v5}, {v3, v6} }

Graph Examples

Consider a social network like Facebook. Users are represented as vertices, and friendships are represented as edges. If A and B are friends, we can have either a directed edge A -> B (meaning A follows B) or an undirected edge {A, B} (meaning they follow each other).

Common Graphs Examples

  • Maps: Represent cities as vertices and roads as edges (weighted to denote distance).
  • World Wide Web: Represent web pages as vertices and hyperlinks as directed edges pointing from source pages to destination pages.
  • File systems: Represent directories as vertices and subdirectory/file connections as edges.

Graph Representations:

  • Adjacency Matrix: Square matrix, where (i,j) reflects the edge weight between vertices i and j. Suitable for dense graphs with many edges.
  • Adjacency List: Array or list of lists, where each index stores connections for a specific vertex. More efficient for sparse graphs with fewer edges.

Implementing Graphs in Java

To start, we’ll implement a simple graph representation in Java. We’ll use an adjacency list to store the graph, which is a list of lists where each list represents the neighbors of a particular vertex.

Java
import java.util.*;

class Graph {
    private int V; // Number of vertices
    private LinkedList<Integer> adjList[]; // Adjacency list representation

    // Constructor
    Graph(int v) {
        V = v;
        adjList = new LinkedList[v];
        for (int i = 0; i < v; ++i)
            adjList[i] = new LinkedList<>();
    }

    // Function to add an edge to the graph
    void addEdge(int v, int w) {
        adjList[v].add(w);
    }

    // Getter for the adjacency list of a vertex
    LinkedList<Integer> getAdjList(int v) {
        return adjList[v];
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        for (int i = 0; i < graph.V; i++) {
            LinkedList<Integer> list = graph.getAdjList(i);
            System.out.print("Adjacency list of vertex " + i + ": ");
            for (Integer node : list) {
                System.out.print(node + " ");
            }
            System.out.println();
        }
    }
}

This Graph class represents an undirected graph using an adjacency list representation. It has a constructor to initialize the graph with a given number of vertices. The addEdge method adds an edge between two vertices. The getAdjList method returns the adjacency list of a specified vertex.

The main method demonstrates how to use the Graph class by creating a graph with 4 vertices and adding some edges. Then, it prints the adjacency list of each vertex.

Common Types of Graphs

Here are some common types of graphs and their java implementations:

Directed Graphs (Digraphs)

Directed graphs, also known as digraphs, are graphs in which edges have a direction associated with them. This means that the relationship between nodes is one-way. They are used to model relationships like dependencies, flows, or hierarchical structures.

Java
   A
  ↗ ↘
 B   C
  ↘ ↗
   D
  • Vertices are represented by letters: A, B, C, D.
  • Arrows indicate the direction of the edges.
  • Arrows point from the source vertex to the destination vertex, representing a directed edge from the source to the destination.

Implementation: In Java, you can implement a directed graph using adjacency lists or adjacency matrices. Here’s a basic example of implementing a directed graph using adjacency lists:

Java
import java.util.*;

class Digraph {
    private int V;
    private List<Integer>[] adj;

    public Digraph(int V) {
        this.V = V;
        adj = new ArrayList[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    public void addEdge(int v, int w) {
        adj[v].add(w);
    }

    public Iterable<Integer> adj(int v) {
        return adj[v];
    }
}

Undirected Graphs

Undirected graphs are graphs in which edges do not have any direction associated with them. The relationship between nodes is bidirectional. They are used to model symmetric relationships, such as friendships in social networks or connections in a network.

Java
   A
  / \
 /   \
B --- C
 \   /
  \ /
   D
  • Vertices are represented by letters: A, B, C, D.
  • Edges are represented by lines connecting the vertices.
  • Each line connects two vertices, representing an undirected edge between them.

Implementation: Similar to directed graphs, undirected graphs can be implemented using adjacency lists or adjacency matrices. Here’s a basic example of implementing an undirected graph using adjacency lists:

Java
import java.util.*;

class Graph {
    private int V;
    private List<Integer>[] adj;

    public Graph(int V) {
        this.V = V;
        adj = new ArrayList[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    public void addEdge(int v, int w) {
        adj[v].add(w);
        adj[w].add(v);
    }

    public Iterable<Integer> adj(int v) {
        return adj[v];
    }
}

Weighted Graphs

Weighted graphs are graphs in which each edge has an associated weight or cost. They are used to model scenarios where the relationship between nodes has a numerical value associated with it, such as distances between cities or costs of transportation.

Java
   
   A
2/   \4
/     \
B--5-- C
\1   3/
 \   /
   D
  • Vertices are represented by letters: A, B, C, D.
  • Edges are represented by lines connecting the vertices.
  • Each line connecting two vertices represents an edge.
  • Beside each edge, there is a number indicating the weight of that edge.
  • For example, the edge between vertices A and B has a weight of 2, the edge between vertices B and D has a weight of 1, etc.

Implementation: Weighted graphs can be implemented similarly to undirected or directed graphs, with the addition of storing weights along with edges. Here’s an example of implementing a weighted graph using adjacency lists:

Java
import java.util.*;

class WeightedGraph {
    private int V;
    private List<Edge>[] adj;

    class Edge {
        int v, w;

        Edge(int v, int w) {
            this.v = v;
            this.w = w;
        }
    }

    public WeightedGraph(int V) {
        this.V = V;
        adj = new ArrayList[V];
        for (int i = 0; i < V; i++) {
            adj[i] = new ArrayList<>();
        }
    }

    public void addEdge(int v, int w, int weight) {
        adj[v].add(new Edge(w, weight));
        adj[w].add(new Edge(v, weight)); // For undirected graphs
    }

    public Iterable<Edge> adj(int v) {
        return adj[v];
    }
}

Graph Algorithms

Now that we have our graph implementation ready, let’s explore some fundamental graph algorithms.

Depth-First Search (DFS) Traversal Algorithm

DFS is a graph traversal algorithm that explores as far as possible along each branch before backtracking. It uses a stack (or recursion) to keep track of vertices to visit next.

Java
class DFS {
    // Depth-First Search traversal
    void dfsUtil(int v, boolean visited[], Graph graph) {
        visited[v] = true;
        System.out.print(v + " ");

        LinkedList<Integer> neighbors = graph.getAdjList(v);
        for (Integer neighbor : neighbors) {
            if (!visited[neighbor])
                dfsUtil(neighbor, visited, graph);
        }
    }

    void dfs(int v, Graph graph) {
        boolean visited[] = new boolean[graph.V];
        dfsUtil(v, visited, graph);
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        DFS dfs = new DFS();
        System.out.print("Depth-First Search traversal starting from vertex 2: ");
        dfs.dfs(2, graph);
    }
}

This DFS class contains methods for performing a Depth-First Search (DFS) traversal on a graph. The dfsUtil method is a recursive utility function that performs DFS traversal starting from a specified vertex (v). It marks the vertex as visited, prints its value, and then recursively visits its unvisited neighbors. The dfs method initializes an array to track visited vertices and calls dfsUtil to start the DFS traversal from a specified vertex.

The main method demonstrates how to use the DFS class by creating a graph with 4 vertices and adding some edges. Then, it performs a DFS traversal starting from vertex 2 and prints the traversal result.

Breadth-First Search (BFS) Traversal Algorithm

BFS is another traversal algorithm that explores all the neighboring nodes at the present depth prior to moving on to the nodes at the next depth level.

Java
import java.util.LinkedList;

class BFS {
    // Breadth-First Search traversal
    void bfs(int v, Graph graph) {
        boolean visited[] = new boolean[graph.V];
        LinkedList<Integer> queue = new LinkedList<>();

        visited[v] = true;
        queue.add(v);

        while (!queue.isEmpty()) {
            v = queue.poll();
            System.out.print(v + " ");

            LinkedList<Integer> neighbors = graph.getAdjList(v);
            for (Integer neighbor : neighbors) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.add(neighbor);
                }
            }
        }
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        BFS bfs = new BFS();
        System.out.print("Breadth-First Search traversal starting from vertex 2: ");
        bfs.bfs(2, graph);
    }
}

This BFS class contains a method for performing a Breadth-First Search (BFS) traversal on a graph. The bfs method initializes a boolean array to track visited vertices and a queue to store vertices for traversal. It starts the traversal from a specified vertex (v), marks it as visited, and adds it to the queue. Then, it repeatedly dequeues vertices from the queue, prints them, and adds their unvisited neighbors to the queue.

The main method demonstrates how to use the BFS class by creating a graph with 4 vertices and adding some edges. Then, it performs a BFS traversal starting from vertex 2 and prints the traversal result.

Dijkstra’s Shortest Path Algorithm

Dijkstra’s algorithm finds the shortest path from a source vertex to all other vertices in a weighted graph. It maintains a priority queue to continuously select the vertex with the smallest distance from the source.

Java
import java.util.*;

class Dijkstra {
    // Dijkstra's Shortest Path Algorithm
    void dijkstra(int src, Graph graph) {
        int V = graph.V;
        int[] dist = new int[V];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[src] = 0;

        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.comparingInt(i -> dist[i]));
        pq.add(src);

        while (!pq.isEmpty()) {
            int u = pq.poll();
            LinkedList<Integer> neighbors = graph.getAdjList(u);
            for (Integer v : neighbors) {
                int weight = 1; // Assuming unweighted graph, modify for weighted graph
                if (dist[u] + weight < dist[v]) {
                    dist[v] = dist[u] + weight;
                    pq.add(v);
                }
            }
        }

        // Print shortest distances
        System.out.println("Shortest distances from source vertex " + src + ":");
        for (int i = 0; i < V; i++) {
            System.out.println("Vertex " + i + ": " + dist[i]);
        }
    }

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(1, 3);
        graph.addEdge(2, 4);
        graph.addEdge(3, 4);
        graph.addEdge(3, 5);
        graph.addEdge(4, 5);

        Dijkstra dijkstra = new Dijkstra();
        int sourceVertex = 0;
        dijkstra.dijkstra(sourceVertex, graph);
    }
}

This Dijkstra class contains a method for finding the shortest path distances from a source vertex to all other vertices in a graph using Dijkstra’s algorithm. The dijkstra method initializes an array to store shortest distances (dist), initializes all distances to infinity except for the source vertex distance which is set to 0. It then uses a priority queue (pq) to greedily select the vertex with the shortest distance and updates the distances to its neighbors if a shorter path is found. Finally, it prints the shortest distances from the source vertex to all other vertices.

The main method demonstrates how to use the Dijkstra class by creating a graph with 6 vertices and adding some edges. Then, it performs Dijkstra’s algorithm starting from a specified source vertex and prints the shortest distances to all other vertices.

Detecting Cycles in a Directed Graph

Detecting cycles in a directed graph is crucial in various applications such as deadlock detection and task scheduling. We’ll use a modified DFS approach to detect cycles.

Java
class CycleDetection {
    boolean isCyclicUtil(int v, boolean visited[], boolean recStack[], Graph graph) {
        if (recStack[v])
            return true;

        if (visited[v])
            return false;

        visited[v] = true;
        recStack[v] = true;

        LinkedList<Integer> neighbors = graph.getAdjList(v);
        for (Integer neighbor : neighbors) {
            if (isCyclicUtil(neighbor, visited, recStack, graph))
                return true;
        }

        recStack[v] = false;
        return false;
    }

    boolean isCyclic(Graph graph) {
        boolean visited[] = new boolean[graph.V];
        boolean recStack[] = new boolean[graph.V];

        for (int i = 0; i < graph.V; i++) {
            if (isCyclicUtil(i, visited, recStack, graph))
                return true;
        }

        return false;
    }

    public static void main(String[] args) {
        Graph graph = new Graph(4);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 2);
        graph.addEdge(2, 0);
        graph.addEdge(2, 3);
        graph.addEdge(3, 3);

        CycleDetection cycleDetection = new CycleDetection();
        if (cycleDetection.isCyclic(graph))
            System.out.println("The graph contains a cycle");
        else
            System.out.println("The graph doesn't contain a cycle");
    }
}

This CycleDetection class contains methods to detect cycles in a graph using depth-first search (DFS). The isCyclicUtil method recursively checks if there is a cycle starting from a given vertex (v) by maintaining a boolean array (recStack) to keep track of the vertices in the current recursion stack. If a vertex is visited and also present in the recursion stack, it indicates the presence of a cycle. The isCyclic method iterates through all vertices in the graph and calls isCyclicUtil for each vertex to check for cycles.

The main method demonstrates how to use the CycleDetection class by creating a graph with 4 vertices and adding some edges. Then, it checks if the graph contains a cycle and prints the result.

Topological Sorting of a Graph

Topological sorting is a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u → v, vertex u comes before vertex v in the ordering.

Java
import java.util.*;

class TopologicalSort {
    Stack<Integer> stack = new Stack<>();

    void topologicalSortUtil(int v, boolean visited[], Graph graph) {
        visited[v] = true;

        LinkedList<Integer> neighbors = graph.getAdjList(v);
        for (Integer neighbor : neighbors) {
            if (!visited[neighbor])
                topologicalSortUtil(neighbor, visited, graph);
        }

        stack.push(v);
    }

    void topologicalSort(Graph graph) {
        int V = graph.V;
        boolean visited[] = new boolean[V];

        for (int i = 0; i < V; i++) {
            if (!visited[i])
                topologicalSortUtil(i, visited, graph);
        }

        // Print topological order
        System.out.println("Topological order:");
        while (!stack.isEmpty())
            System.out.print(stack.pop() + " ");
    }

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(5, 2);
        graph.addEdge(5, 0);
        graph.addEdge(4, 0);
        graph.addEdge(4, 1);
        graph.addEdge(2, 3);
        graph.addEdge(3, 1);

        TopologicalSort topologicalSort = new TopologicalSort();
        topologicalSort.topologicalSort(graph);
    }
}

This TopologicalSort class contains methods to perform topological sorting on a directed acyclic graph (DAG) using depth-first search (DFS). The topologicalSortUtil method recursively traverses the graph, marking visited vertices and pushing them onto a stack after visiting all of their neighbors. The topologicalSort method initiates DFS traversal for each vertex and then prints the topological order by popping vertices from the stack.

The main method demonstrates how to use the TopologicalSort class by creating a graph with 6 vertices and adding directed edges between them. Then, it performs topological sorting and prints the topological order.

Conclusion

In this blog post, we explored several essential graph algorithms implemented in Java, including DFS, BFS, Dijkstra’s Shortest Path Algorithm, cycle detection in directed graphs, and topological sorting. Understanding these algorithms is crucial for tackling a wide range of graph-related problems efficiently. Feel free to experiment with these implementations and explore further applications in your projects. Happy coding!

Binary Tree in Java

Unlocking the Power of Binary Trees: A Comprehensive Guide to Implementing and Exploring in Java

Binary trees are a fundamental data structure in computer science, widely used for representing hierarchical data. In this blog post, we will delve into the concept of binary trees, their implementation in Java, traversal algorithms, and some common operations associated with them.

Understanding Binary Trees

What is a Tree?

A tree is a non-linear data structure used for storing data. It is comprised of nodes and edges without any cycles. Each node in a tree can point to n number of other nodes within the tree. It serves as a way to represent hierarchical structures, with a parent node called the root and multiple levels of additional nodes.

  • It’s a non-linear data structure used for storing data
  • It is made up of nodes and edges without having any cycle. // its so important
  • Each node in a tree can point to n number of nodes in a tree
  • It is a way of representing a hierarchical structure with a parent node called a root and many levels of additional nodes

What is a Binary Tree?

A binary tree is a hierarchical data structure composed of nodes, where each node has at most two children, referred to as the left child and the right child. The topmost node of the tree is called the root node. Each node contains a data element and references to its left and right children (or null if the child does not exist).

In simple words, A tree is called a binary tree if each node has zero, one, or two children.

Java
       1
      / \
     2   3
    / \
   4   5

Binary trees are commonly used in various applications such as expression trees, binary search trees, and more.

How to represent a Binary Tree in java?

TreeNode Representation:

Java
null<-----|left|data|right|------>null

Structure of TreeNode in a Binary Tree:

To represent a binary tree in Java, we can create a TreeNode class to define the structure of each node in the tree. Here’s how we can do it:

Java
public class TreeNode {
    private int data;      // Data of the node
    private TreeNode left; // Pointer to the left child node
    private TreeNode right; // Pointer to the right child node
    
    // Constructor to initialize the node with data
    public TreeNode(int data) {
        this.data = data;
        this.left = null;
        this.right = null;
    }
    
    // Getters and setters for the data, left, and right pointers
    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public TreeNode getLeft() {
        return left;
    }

    public void setLeft(TreeNode left) {
        this.left = left;
    }

    public TreeNode getRight() {
        return right;
    }

    public void setRight(TreeNode right) {
        this.right = right;
    }
}

In this TreeNode class:

  • Each node has a data field to store the value of the node.
  • Each node has a left field to store a reference to its left child node.
  • Each node has a right field to store a reference to its right child node.
  • The constructor initializes the node with the given data and sets both left and right pointers to null initially.

With this TreeNode class, you can create instances of the TreeNode class to represent each node in the binary tree. You can then use these instances to build the binary tree by setting the left and right pointers of each node accordingly. The root of the binary tree is typically represented by a reference to the topmost node in the tree. Initially, if the tree is empty, the root reference is set to null. As you add nodes to the tree, you update the root reference accordingly.

In simple words, Initially, the root node points to null. When we create any temporary node to insert into the tree, such as a temporary node with 15 as its data and left and right node pointers pointing to null, we can then add more nodes to the left or right.

How to implement a binary tree in java?

Let’s start by implementing a basic binary tree structure in Java:

Java
public class BinaryTree {

  private TreeNode root;      // Created one root node of TreeNode type
 
  private class TreeNode {
    private TreeNode left;
    private TreeNode right;
    private int data;

    public TreeNode(int data) {
       this.data = data;
    }
  }

  public void createBinaryTree() {
     TreeNode first = new TreeNode(1);
     TreeNode second = new TreeNode(2);
     TreeNode third = new TreeNode(3);
     TreeNode fourth = new TreeNode(4);
     TreeNode fifth = new TreeNode(5);

     root = first;     // root ---> first
     first.left = second;
     first.right = third;  // second <--- first ---> third

     second.left = fourth;
     second.right = fifth;
  }

  public static void main(String[] args) {
     BinaryTree bt = new BinaryTree();
     bt.createBinaryTree();
  }
}

This BinaryTree class represents a simple binary tree. It has a nested class TreeNode which represents each node in the binary tree. The createBinaryTree method initializes the tree structure by creating nodes and linking them appropriately. Finally, the main method demonstrates creating an instance of BinaryTree and initializing it.

Traversal Algorithms

Traversal is the process of visiting all the nodes in a tree in a specific order. There are three common methods for traversing binary trees:

  1. In-order traversal: Visit the left subtree, then the root, and finally the right subtree.
  2. Pre-order traversal: Visit the root, then the left subtree, and finally the right subtree.
  3. Post-order traversal: Visit the left subtree, then the right subtree, and finally the root.

Let’s implement these traversal algorithms in Java.

Pre-Order Traversal Of Binary Tree in Java

  1. Visit the root node.
  2. Traverse the left subtree in pre-order fashion.
  3. Traverse the right subtree in pre-order fashion.

Recursive Pre-Order Traversal

It recursively traverses the tree in the following order: root, left subtree, right subtree.

Java
public void preOrder(TreeNode root) {
    if(root == null) {
        return;
    }
    System.out.print(root.data + " ");
    preOrder(root.left);
    preOrder(root.right);
}

In this code:

  • If the root is null, indicating an empty subtree, the method returns.
  • If the root is not null, it prints the data of the current node, traverses the left subtree recursively, and then traverses the right subtree recursively.

Consider the following binary tree:

Java
     1
    / \
   2   3
  / \
 4   5

In pre-order traversal, the order of node visits would be: 1, 2, 4, 5, 3.

Here’s how the execution progresses:

  1. pre_order_traversal(1) – Visit root node 1.
  2. pre_order_traversal(2) – Visit root node 2.
  3. pre_order_traversal(4) – Visit root node 4.
  4. pre_order_traversal(None) – Backtrack to the previous call (pre_order_traversal(4)), finish left subtree traversal.
  5. pre_order_traversal(5) – Visit root node 5.
  6. pre_order_traversal(None) – Backtrack to the previous call (pre_order_traversal(5)), finish left subtree traversal.
  7. pre_order_traversal(None) – Backtrack to the previous call (pre_order_traversal(2)), finish left subtree traversal.
  8. pre_order_traversal(3) – Visit root node 3.
  9. pre_order_traversal(None) – Backtrack to the previous call (pre_order_traversal(3)), finish right subtree traversal.
  10. pre_order_traversal(None) – Backtrack to the previous call (pre_order_traversal(1)), finish right subtree traversal.

And the output of the traversal will be:

Java
Pre-order traversal: 1 2 4 5 3

In this execution, we maintain a call stack. For each method call, we track the line number and the root node being passed to that method. When we reach the base condition, we backtrack to the previous calls and execute the next steps.

Iterative Pre-Order Traversal Of Binary Tree

Iterative pre-order traversal is a way to traverse a binary tree iteratively, without using recursion. we utilize the stack data structure. Initially, we push the root node onto the stack. While the stack is not empty, we iterate using a while loop and perform pre-order traversal operations. As long as the stack is not empty, we pop the top node from the stack and assign it to the temp node. We then print its data. Since in pre-order traversal, we first visit the left node and then the right node, while pushing nodes into the stack, we need to push the right node first and then the left node so that we process the left node first (due to LIFO).

Java
public void IterativePreOrder() {
    if(root == null) {
        return;
    }

    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while(!stack.isEmpty()) {
        TreeNode temp = stack.pop();
        System.out.println(temp.data);
        if(temp.right != null) {
            stack.push(temp.right);
        }
        if(temp.left != null) {
            stack.push(temp.left);
        }
    }
}

This method iterativePreOrder performs an iterative pre-order traversal of the binary tree. It starts from the root node and uses a stack to keep track of nodes to be visited. It prints the data of each node as it visits them. If a node has a right child, it is pushed onto the stack first so that it is visited after the left child (since pre-order traversal visits the left subtree before the right subtree). Finally, the method returns once all nodes have been visited.

Consider the following binary tree:

Java
       1
      / \
     2   3
    / \
   4   5

The iterative pre-order traversal will visit the nodes in the order: 1, 2, 4, 5, 3.

Here’s how the execution goes step by step:

  1. Start with an empty stack and initialize the current node as the root.
  2. Push the root node onto the stack.
  3. While the stack is not empty, do the following:
    • a. Pop the top node from the stack and process it (print its value or perform any other desired operation).
    • b. Push the right child onto the stack if it exists.
    • c. Push the left child onto the stack if it exists.

Let’s execute the algorithm step by step:

  1. Start with an empty stack: Stack = []
  2. Push the root node (1) onto the stack: Stack = [1]
  3. Pop the top node from the stack (1), process it: Output = [1], Stack = []
  4. Push the right child (3) onto the stack: Stack = [3]
  5. Push the left child (2) onto the stack: Stack = [3, 2]
  6. Pop the top node from the stack (2), process it: Output = [1, 2], Stack = [3]
  7. Push the right child (5) onto the stack: Stack = [3, 5]
  8. Push the left child (4) onto the stack: Stack = [3, 5, 4]
  9. Pop the top node from the stack (4), process it: Output = [1, 2, 4], Stack = [3, 5]
  10. There’s no left or right child for node 4, so proceed.
  11. Pop the top node from the stack (5), process it: Output = [1, 2, 4, 5], Stack = [3]
  12. There’s no left or right child for node 5, so proceed.
  13. Pop the top node from the stack (3), process it: Output = [1, 2, 4, 5, 3], Stack = []
  14. The stack is empty, so the traversal is complete.

The output of the iterative traversal is: [1, 2, 4, 5, 3], which is the pre-order traversal of the given binary tree.

During the execution of iterative pre-order traversal, we follow the described algorithm. We push the root node onto the stack initially. Then, as long as the stack is not empty, we pop nodes from the stack, print their data, and push their right and left children onto the stack accordingly.

In Order Binary Tree traversal

  1. Traverse the left subtree in In-order fashion.
  2. Visit the root node.
  3. Traverse the right subtree in In-order fashion.

In-order traversal of a binary tree involves visiting the left subtree in in-order fashion, then visiting the root node, and finally traversing the right subtree in in-order fashion.

Recursive In-Order Binary Tree Traversal

In Recursive In-order traversal, we visit each node in the tree in the following order:

  1. Recursively traverse the left subtree.
  2. Visit the root node.
  3. Recursively traverse the right subtree.
Java
public void RecursiveInOrder(TreeNode root) {
    if(root == null) { // base case
        return;
    }
    
    RecursiveInOrder(root.left);
    System.out.print(root.data + " ");
    RecursiveInOrder(root.right);
}

In this code:

  • If the root is null, indicating an empty subtree, the method returns.
  • If the root is not null, it recursively traverses the left subtree, then prints the data of the current node, and finally recursively traverses the right subtree.
  • This process effectively traverses the tree in an in-order sequence.

Consider the same binary tree:

Java
     1
    / \
   2   3
  / \
 4   5

In in-order traversal, the order of node visits would be: 4, 2, 5, 1, 3.

Here’s how the execution progresses:

  1. in_order_traversal(1) – Traverse left subtree.
  2. in_order_traversal(2) – Traverse left subtree.
  3. in_order_traversal(4) – Visit root node 4.
  4. in_order_traversal(None)Backtrack to the previous call (in_order_traversal(4)), finish left subtree traversal.
  5. Print root node 2.
  6. in_order_traversal(5) – Visit root node 5.
  7. in_order_traversal(None)Backtrack to the previous call (in_order_traversal(5)), finish left subtree traversal.
  8. Print root node 1.
  9. in_order_traversal(3) – Traverse right subtree.
  10. Print root node 3.
  11. in_order_traversal(None)Backtrack to the previous call (in_order_traversal(1)), finish right subtree traversal.

And the output of the traversal will be:

Java
In-order traversal:4 2 5 1 3

During the execution of recursive in-order traversal, we maintain a method call stack. For each method call, we keep track of the line number and the root node being passed to that method. Initially, we pass the root node to this method. Then, we visit the left subtree of the root node, print the root node’s data, and finally visit the right subtree of the root node. This tracking of each recursive method call, line number, and root node helps during backtracking.

Iterative In Order Traversal of binary tree in java

In iterative in-order traversal, we visit the left node first, then perform in-order traversal for that node, come back to print the root node’s data, and finally visit the right node to perform its in-order traversal.

To achieve this, we create a stack of TreeNodes and initialize a temp node with the root node. We traverse the tree until the stack is not empty or temp is not null (indicating there are nodes to traverse or backtrack).

If temp is not null, we push it onto the stack and move to its left child. If temp is null, indicating we have reached a leaf node, we pop the top node from the stack, print its data, and move to its right child. This process continues until all nodes are traversed.

Java
public void iterativeInOrderTraversal(TreeNode root) {
   if (root == null) {
     return;
   }

  Stack<TreeNode> stack = new Stack<>();
  TreeNode temp = root;
  while (!stack.isEmpty() || temp != null) {
    if (temp != null) {
      stack.push(temp);
      temp = temp.left;
    } else {
      temp = stack.pop();
      System.out.print(temp.data + " ");
      temp = temp.right;
    }    
  }
}

This method iterativeInOrderTraversal performs an iterative in-order traversal of the binary tree. It starts from the root node and uses a stack to keep track of nodes to be visited. It traverses the left subtree first, then visits the current node, and finally traverses the right subtree. If a node has a left child, it is pushed onto the stack and the left child becomes the current node. If a node does not have a left child or has already been visited, it is popped from the stack, its data is printed, and its right child becomes the current node. The traversal continues until all nodes have been visited and the stack is empty.

Consider the same binary tree:

Java
       1
      / \
     2   3
    / \
   4   5

The iterative in-order traversal will visit the nodes in the order: 4, 2, 5, 1, 3.

Here’s how the execution goes step by step:

  1. Start with an empty stack and initialize the current node as the root.
  2. Push the root node onto the stack and move to the left child until reaching a null node.
  3. When a null node is reached, pop the top node from the stack, process it (print its value or perform any other desired operation), and move to its right child.

Let’s execute the algorithm step by step:

  1. Start with an empty stack: Stack = []
  2. Push the root node (1) onto the stack and move to the left child: Stack = [1], Current Node = 1
  3. Move to the left child of 1 (2) and push it onto the stack: Stack = [1, 2], Current Node = 2
  4. Move to the left child of 2 (4) and push it onto the stack: Stack = [1, 2, 4], Current Node = 4
  5. The left child of 4 is null, so pop 4 from the stack, process it: Output = [4], Stack = [1, 2], Current Node = 2
  6. Move to the right child of 4 (null), so no action needed: Output = [4], Stack = [1, 2], Current Node = 2
  7. Process node 2: Output = [4, 2], Stack = [1], Current Node = 2
  8. Move to the right child of 2 (5) and push it onto the stack: Stack = [1, 5], Current Node = 5
  9. The left child of 5 is null, so process node 5: Output = [4, 2, 5], Stack = [1], Current Node = 5
  10. Move to the right child of 5 (null), so no action needed: Output = [4, 2, 5], Stack = [1], Current Node = 5
  11. Process node 1: Output = [4, 2, 5, 1], Stack = [], Current Node = 1
  12. Move to the right child of 1 (3) and push it onto the stack: Stack = [3], Current Node = 3
  13. The left child of 3 is null, so process node 3: Output = [4, 2, 5, 1, 3], Stack = [], Current Node = 3
  14. Move to the right child of 3 (null), so no action needed: Output = [4, 2, 5, 1, 3], Stack = [], Current Node = 3
  15. The stack is empty, so the traversal is complete.

The output of the traversal is: [4, 2, 5, 1, 3], which is the in-order traversal of the given binary tree.

Post Order traversal of Binary Tree

  1. Traverse the left subtree in Post-order fashion.
  2. Traverse the right subtree in Post-order fashion.
  3. Visit the root node.

Recursive Post-Order Traversal of Binary Tree

In recursive post-order traversal, we visit each node in the tree in the following order:

  1. Recursively traverse the left subtree.
  2. Recursively traverse the right subtree.
  3. Visit the root node.
Java
       1
      / \
     2   3
    / \
   4   5

In post-order traversal, the order of node visits would be: 4, 5, 2, 3, 1.

Java
public void RecursivePostOrder(TreeNode root) {
    if(root == null) {
        return;
    }
    
    RecursivePostOrder(root.left);
    RecursivePostOrder(root.right);
    System.out.print(root.data + " ");
}

In this code:

  • If the root is null, indicating an empty subtree, the method returns.
  • If the root is not null, it recursively traverses the left subtree, then recursively traverses the right subtree, and finally prints the data of the current node.
  • This process effectively traverses the tree in a post-order sequence.

Here’s how the execution progresses:

  1. post_order_traversal(1) – Traverse left subtree.
  2. post_order_traversal(2) – Traverse left subtree.
  3. post_order_traversal(4) – Visit root node 4.
  4. post_order_traversal(None) – Backtrack to the previous call (post_order_traversal(4)), finish left subtree traversal.
  5. post_order_traversal(5) – Visit root node 5.
  6. post_order_traversal(None) – Backtrack to the previous call (post_order_traversal(5)), finish left subtree traversal.
  7. Print root node 2.
  8. post_order_traversal(3) – Traverse right subtree.
  9. Print root node 3.
  10. post_order_traversal(None) – Backtrack to the previous call (post_order_traversal(3)), finish right subtree traversal.
  11. Print root node 1.
  12. post_order_traversal(None) – Backtrack to the previous call (post_order_traversal(1)), finish right subtree traversal.

And the output of the traversal will be:

Java
Post-order traversal: 4 5 2 3 1

In recursive post-order traversal, we utilize a method call stack. For each recursive call, we visit the left and right nodes respectively, and then print the data of the root node.

To illustrate this, we maintain a method call stack where we track the line number and the root node being passed to that method. This approach ensures that we visit the left and right subtrees first before printing the data of the root node.

Iterative post-order traversal of a binary tree

Iterative post-order traversal is a bit more complex compared to pre-order and in-order traversals because you need to ensure that you visit the left and right subtrees before processing the root node. One common approach to achieve this iteratively is to use two stacks.

Java
public void iterativePostOrderTraversal(TreeNode root) {
   if (root == null) {
     return;
   }

   Stack<TreeNode> stack1 = new Stack<>();
   Stack<TreeNode> stack2 = new Stack<>();
   stack1.push(root);

   while (!stack1.isEmpty()) {
       TreeNode temp = stack1.pop();
       stack2.push(temp);
       if (temp.left != null) {
           stack1.push(temp.left);
       }
       if (temp.right != null) {
           stack1.push(temp.right);
       }
   }

   while (!stack2.isEmpty()) {
       System.out.print(stack2.pop().data + " ");
   }
}

This method iterativePostOrderTraversal performs an iterative post-order traversal of the binary tree. It starts from the root node and uses two stacks to keep track of nodes to be visited. It pushes nodes onto stack2 in a pre-order traversal (root, right, left) using stack1. After traversing all nodes, it pops nodes from stack2 to print their data, resulting in a post-order traversal (left, right, root). The traversal continues until all nodes have been visited and both stacks are empty.

Here’s how the execution goes step by step:

  1. Start with two empty stacks: Stack1 and Stack2. Initialize the current node as the root.
  2. Push the root node onto Stack1.
  3. While Stack1 is not empty, do the following: a. Pop the top node from Stack1 and push it onto Stack2. b. Push the left child onto Stack1 if it exists. c. Push the right child onto Stack1 if it exists.
  4. Once Stack1 is empty, all nodes have been pushed onto Stack2. Pop nodes from Stack2 to get the post-order traversal.

Let’s execute the algorithm step by step:

  1. Start with two empty stacks: Stack1 = [], Stack2 = [].
  2. Push the root node (1) onto Stack1: Stack1 = [1].
  3. Pop the top node from Stack1 (1) and push it onto Stack2: Stack2 = [1].
  4. Push the right child (3) onto Stack1: Stack1 = [3].
  5. Push the left child (2) onto Stack1: Stack1 = [3, 2].
  6. Pop the top node from Stack1 (2) and push it onto Stack2: Stack2 = [1, 2].
  7. Push the right child (5) onto Stack1: Stack1 = [3, 5].
  8. Push the left child (4) onto Stack1: Stack1 = [3, 5, 4].
  9. Pop the top node from Stack1 (4) and push it onto Stack2: Stack2 = [1, 2, 4].
  10. Pop the top node from Stack1 (5) and push it onto Stack2: Stack2 = [1, 2, 4, 5].
  11. Pop the top node from Stack1 (3) and push it onto Stack2: Stack2 = [1, 2, 4, 5, 3].
  12. All nodes have been processed and pushed onto Stack2.
  13. Pop nodes from Stack2 to get the post-order traversal: Output = [4, 5, 2, 3, 1].

The output of the traversal is: [4, 5, 2, 3, 1], which is the post-order traversal of the given binary tree.

Level order traversal of a Binary Tree in Java

In level order traversal, also known as breadth-first traversal, we visit each node level by level. To achieve this, we use a queue data structure. Initially, we add the root node to the queue. Then, we iterate while the queue is not empty.

During each iteration, we dequeue a node from the queue and assign it to the temp node. We print the data of this node. If the temp node has left and/or right children, we enqueue them into the queue. This ensures that we traverse the tree level by level, visiting each node before moving on to its children.

This approach is useful for applications like level-wise printing of a binary tree or finding the shortest path in a graph represented as a tree.

Java
if (root == null) {
   return;
}

Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
  TreeNode temp = queue.poll();
  System.out.print(temp.data + " ");
  if (temp.left != null) {
    queue.offer(temp.left);
  }
  if (temp.right != null) {
    queue.offer(temp.right);
  }
}


// o/p : 1,2,3,4,5

This code performs a level-order traversal of the binary tree using a queue. It starts from the root node and adds it to the queue. Then, it repeatedly dequeues nodes from the queue, prints their data, and enqueues their left and right children if they exist. This process continues until all nodes in the tree have been visited.

Bonus: Binary Search in Java

Binary search in Java is a searching algorithm commonly applied to arrays or lists. It is used to find the position of a target value within a sorted array or list.

Binary trees, on the other hand, are a data structure consisting of nodes in a hierarchy, where each node has at most two children, referred to as the left and right child. Binary search trees (BSTs) are a specific type of binary tree where the values are stored in a sorted order, and each node’s value is greater than all values in its left subtree and less than all values in its right subtree.

While binary search and binary trees share the term “binary,” they are distinct concepts and are not directly related in terms of implementation. However, binary search trees leverage the principles of binary search to efficiently search, insert, and delete elements. The property of binary search trees ensures that searching for a value can be performed efficiently, similar to binary search in sorted arrays.

Here’s the explanation and execution of the binary search algorithm in Java

Java
int low = 0;
int high = nums.length - 1;
while(low <= high) {
  int mid = (high + low) / 2;
  if(nums[mid] == key) {
    return mid;
  }

  if(key < nums[mid]) {
    high = mid - 1;
  } else {
    low = mid + 1;
  }
}
return -1;

In binary search, we look into the array until the low index becomes greater than the high index. We iterate until this condition holds true.

First, we find the mid index of the array using the formula (high + low) / 2.

If we find the key at the exact mid position, we return that mid index.

If the key is less than the value at the mid index, it means our key is in the first half of the array. So, we update high to mid - 1 and continue searching in that half.

If the key is greater than the value at the mid index, it means our key is in the right side of the array. So, we update low to mid + 1 and continue searching in that half.

If we don’t find the key in the array, we return -1, indicating that the key is not present in the array.

Let’s execute the algorithm step by step:

Java
nums[]: 1, 10, 20, 47, 59, 65, 75, 88, 99
key: 65

low = 0, high = 8
Iteration 1:
  mid = (0 + 8) / 2 = 4
  nums[mid] = 59
  Since key (65) > nums[mid], update low = mid + 1 = 4 + 1 = 5

low = 5, high = 8
Iteration 2:
  mid = (5 + 8) / 2 = 6
  nums[mid] = 75
  Since key (65) < nums[mid], update high = mid - 1 = 6 - 1 = 5

low = 5, high = 5
Iteration 3:
  mid = (5 + 5) / 2 = 5
  nums[mid] = 65
  Since key (65) == nums[mid], return mid = 5 (key found at index 5)

Key 65 found at index 5 in the array.

Code

Java
public class BinarySearch {
   
  public int binarySearch(int[] nums, int key) {
     int low = 0;
     int high = nums.length - 1;
     while (low <= high) {
       int mid = (high + low) / 2;
       if (nums[mid] == key) {
          return mid;
       }
       if (key < nums[mid]) {
          high = mid - 1;
       } else {
          low = mid + 1;
       }
    }
    return -1;
  }

  public static void main(String[] args) {
     BinarySearch bs = new BinarySearch();
     int[] nums = {1, 10, 20, 47, 59, 65, 75, 88, 99};
     int key = 65;
     System.out.println(bs.binarySearch(nums, key));
  }
}

This BinarySearch class implements the binary search algorithm to find the index of a key in a sorted array. The binarySearch method takes an array of integers (nums) and a key to search for. It initializes the low and high pointers to the start and end indices of the array, respectively. Then, it iteratively calculates the mid index, compares the key with the element at the mid index, and updates the low and high pointers accordingly based on whether the key is smaller or larger than the mid element. If the key is found, the method returns the index of the key; otherwise, it returns -1. The main method demonstrates how to use the binarySearch method to search for a key in a sorted array and prints the index of the key.

Conclusion

Binary trees are powerful data structures that offer efficient storage and retrieval of hierarchical data. Understanding their implementation and traversal algorithms is crucial for any Java programmer. By mastering binary trees, you’ll be equipped to tackle a wide range of programming challenges that involve hierarchical data representation and manipulation.

Doubly Linked List

Mastering Doubly Linked Lists in Java: A Comprehensive Guide

Doubly Linked Lists are a fundamental data structure in computer science, particularly in Java programming. They offer efficient insertion, deletion, and traversal operations compared to other data structures like arrays or singly linked lists. In this blog, we’ll delve into the concept of doubly linked lists, their implementation in Java, and explore some common operations associated with them.

What is a Doubly Linked List?

A Doubly Linked List is a type of linked list where each node contains a data element and two references or pointers: one pointing to the next node in the sequence, and another pointing to the previous node. This bidirectional linkage allows traversal in both forward and backward directions, making operations like insertion and deletion more efficient compared to singly linked lists.

Each node in a doubly linked list typically has the following structure:

Java
class Node {
    int data;
    Node prev;
    Node next;
}

Here, data represents the value stored in the node, prev is a reference to the previous node, and next is a reference to the next node.

How to represent Doubly Linked List in java?

Doubly Linked List:

  1. It is called a two-way linked list.
  2. Given a node, we can navigate the list in both forward and backward directions, which is not possible in a singly linked list.
  3. In a singly linked list, a node can only be deleted if we have a pointer to its previous node. However, in a doubly linked list, we can delete the node even if we don’t have a pointer to its previous node.
  4. ListNode in a Doubly Linked List:
Java
<------| previous | data | next |------->

In this visual representation:

  • <------ indicates the direction of the previous pointers.
  • |prev| represents the pointer to the previous node.
  • |data| represents the data stored in the node.
  • |next| represents the pointer to the next node.
  • -------> indicates the direction of the next pointers.

Each node in the doubly linked list contains pointers to both the previous and next nodes, allowing bidirectional traversal.

ListNode in a Doubly Linked List:

Java
public class ListNode {
    int data;
    ListNode previous;
    ListNode next;

    public ListNode(int data) {
        this.data = data;
    }
}

Doubly Linked List:

Java
null <-----head-----> 1 <-------> 10 <-------> 15 <-------> 65 <------tail-----> null

Doubly Linked List node has two pointers: previous and next. The list has two pointers:

  • head points to the first node.
  • tail points to the last node of the list.

How to implement Doubly Linked List in java ?

Java
public class DoublyLinkedList {
  private ListNode head;
  private ListNode tail;
  private int length;

  private class ListNode {
    private int data;
    private ListNode next;
    private ListNode previous;

    public ListNode(int data) {
       this.data = data;
    }
  }

  public DoublyLinkedList() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  public boolean isEmpty() {
    return length == 0;    // head == null
  }

  public int length() {
    return length;
  }
}

This Java class DoublyLinkedList defines a doubly linked list. It includes inner class ListNode for representing each node in the list. The class provides methods to check if the list is empty and to get the length of the list. The constructor initializes the head and tail pointers to null and sets the length to 0.

Common Operations of Doubly Linked List in java

Here are some common operations performed on a doubly linked list:

  1. Insertion: Elements can be inserted at the beginning, end, or at any specific position within the list.
  2. Deletion: Elements can be deleted from the list based on their value or position.
  3. Traversal: Traversing the list from the beginning to the end or vice versa.
  4. Search: Searching for a specific element within the list.
  5. Reverse: Reversing the order of elements in the list.

How to print elements of Doubly Linked List in java?

Java
null<-----head------> 1 <-------> 10 <-------> 15 <-------> 25 <------tail------>null

Here, we use two algorithms to print data:

i) It will start from the head and print data until the end, i.e., Forward Direction.

ii) It will start from the tail and print data until the end, i.e., Backward Direction.

Forward Direction Print

Java
ListNode temp = head;
while(temp != null)
{
   System.out.print(temp.data + "-->");
   temp = temp.next;
}
System.out.println("null");

Here, we use a temporary node for traversing purposes. We assign the head node to it, i.e., ListNode temp = head;. Next, we move until the end of the list from the head. While traversing, we print data, and when we reach the end, our while loop terminates. That’s why we need to print ‘null’ on the next line.

Output: 1–>10–>15–>25–>null

Code

Java
public void displayForward() {
  if (head == null) {
    return;
  }
   
  ListNode temp = head;
  while (temp != null) {
    System.out.print(temp.data + "-->");
    temp = temp.next;
  }
  System.out.println("null");
}

This method displayForward is designed to print the elements of the doubly linked list in the forward direction. It starts from the head node and traverses the list, printing the data of each node followed by an arrow (-->). Finally, it prints null to indicate the end of the list. If the list is empty (i.e., head is null), the method simply returns without performing any operation.

Backward Direction Print

Java
ListNode temp = tail;
while(temp != null)
{
  System.out.print(temp.data + "-->");
  temp = temp.previous;
}
System.out.println("null");

Here, we use a temporary node for traversing purposes. We assign the tail node to it, i.e., ListNode temp = tail;. Next, we move back until the end of the list from the tail. While traversing, we print data, and when we reach the end, our while loop terminates. That’s why we need to print ‘null’ on the next line.

Output: 25 –> 15 –> 10 –> 1 –> null

Code

Java
public void displayBackward() {
   if (tail == null) {
      return;
   }

   ListNode temp = tail;
   while (temp != null) {
      System.out.print(temp.data + "-->");
      temp = temp.previous;
   }
   System.out.println("null");  
}

This method displayBackward is designed to print the elements of the doubly linked list in the backward direction. It starts from the tail node and traverses the list, printing the data of each node followed by an arrow (-->). Finally, it prints null to indicate the end of the list. If the list is empty (i.e., tail is null), the method simply returns without performing any operation.

How to insert node at the beginning of Doubly Linked List?

Java
ListNode newNode = new ListNode(value);
if(isEmpty())
{
   tail = newNode;
}
else
{
   head.previous = newNode;
}

newNode.next = head;
head = newNode;
length++; // as we inserted node successfully increase the length by one

Here, the head node plays a major role, while the tail node is only considered when the list node is empty. At that time, both head and tail point to null. So when we insert a new node, we assign it to the tail. At that time, the new node’s previous and next pointers point to null, and both head and tail point to that new node. But the next time when our list contains nodes, how do we insert at the beginning?

Inserting at the beginning means we need to assign newNode to the head’s previous pointer so that the new node will be before the head node. Still, the new node’s next pointer needs to be assigned to the head so that a two-way connection is built between every node.

Finally, we have successfully inserted the new node at the beginning. Our head should point to the new node, so we update it accordingly. However, our tail remains the same at the last node only.

Java
Output:

null<---|previous|10|next|<------->|previous|1|next|---->null // here head --> 10 and tail --> 1

Code

Java
public void insertFirst(int value) {
   ListNode newNode = new ListNode(value);
   if (isEmpty()) {
      tail = newNode;
   } else {
      head.previous = newNode;
   }
   newNode.next = head;
   head = newNode;
   length++;
}

This method insertFirst is designed to insert a new node with the given value at the beginning of the doubly linked list. If the list is empty, the new node becomes both the head and the tail of the list. Otherwise, the new node is inserted before the current head node, and its next reference is updated to point to the current head node. Finally, the length of the list is incremented.

How to insert node at the end of a Doubly Linked List in Java?

Java
ListNode newNode = new ListNode(value);
if(isEmpty())
{
  head = newNode;
}
else
{
  tail.next = newNode;
  newNode.previous = tail;
}
tail = newNode;

The logic is similar to the insertion at the beginning. Here, the tail pointer plays a major role as it points to the end of the list. When the list is empty, both head and tail will point to null. Now, we create a new node with the given value, but its previous and next pointers point to null. So, when the list is empty and we add a new node at the end of the list, it means we only need to point head and tail to the new node. But from the next insertion onward, adjacent nodes will point to each other, maintaining the two-way nature of the main doubly linked list. Therefore, we will add the new node to the tail node’s next pointer, and the new node’s previous pointer will point to the tail node. Here, the head remains the same, only the tail will change every time.

Finally, after every successful insertion, we need to increment the length by 1.

Java
Output: 

null<-----|previous|1|next|<--------->|previous|10|next|------>null // here head --> 1 and newNode & tail --> 10

Code

Java
public void insertLast(int value) {
   ListNode newNode = new ListNode(value);
   if (isEmpty()) {
      head = newNode;   // If the list is initially empty, assign the new node to the head
   } else {
      // Establish two-way connection between the new node and the current tail node
      tail.next = newNode;
      newNode.previous = tail;
   }
   tail = newNode;     // Update the tail to point to the new node
   length++;           // Increment the length of the list by 1
}

This method insertLast is designed to insert a new node with the given value at the end of the doubly linked list. If the list is initially empty, the new node becomes the head of the list. Otherwise, it establishes a two-way connection between the new node and the current tail node, and updates the tail pointer to point to the new node. Finally, it increments the length of the list by 1.

How to delete first node in doubly linked list in java ?

Java
//Sample Input: 

null<---|previous|1|next|<------>|previous|10|next|<-------->|previous|15|next|--->null      // here head --> 1 and tail --> 15
Java
if(isEmpty())
{
   throw new NoSuchElementException();
}
ListNode temp = head;
if(head == tail)
{
   tail = null;
}
else
{
   head.next.previous = null;
}
head = head.next;
temp.next = null;
length--;
return temp;

Here, the tail will not play any major role because it is at the end of the list, and we want to delete the first node. So, the head will play a major role in this case. We assign the head node to the temporary node ‘temp’ because we want to delete the first node.

If the list has only one node, which is also the last node, it means head and tail should point to the same value. In such cases, the tail will be assigned with null, and the head will move to the next pointer, which may also be null. Additionally, we assign null to the temp’s next node, and return the temp node. Furthermore, we need to reduce the length by 1.

If the list has more than one node, we first move to the head’s next node and then remove its previous node. Then, we change the head to the next node so that we remove the head node. Using the statement ‘temp.next = null;’, we remove the head node and return it. Additionally, we need to reduce the length by 1.

Code

Java
public ListNode deleteFirst() {
     if (isEmpty()) {
         throw new NoSuchElementException();
     }
     ListNode temp = head;
     if (head == tail) {
       tail = null;
     } else {
       head.next.previous = null;
     }
     head = head.next;
     temp.next = null;
     length--;
     return temp;
}

This method deleteFirst is designed to delete the first node of the doubly linked list and return the deleted node. If the list is empty, it throws a NoSuchElementException. Otherwise, it removes the head node from the list. If the list contains only one node (head and tail are the same), it sets the tail to null. Otherwise, it updates the previous reference of the next node after the head to null. Finally, it updates the head pointer to the next node, decrements the length of the list, and returns the deleted node.

How to delete last node in Doubly Linked List in java ?

Java
if(isEmpty())
{
  throw new NoSuchElementException();
}
ListNode temp = tail;
if(head == tail)
{
   head = null;
}
else
{
   tail.previous.next = null;
}
tail = tail.previous;
temp.previous = null;
length--;
return temp;

If the list is empty, we throw a NoSuchElementException.

When the list contains only one node, at that time, both head and tail point to the same node. Here, only head will play a major role; elsewhere, the tail plays an important role. To delete the last node in this case, we assign null to head, and the tail’s previous will become the new tail. Then, we delete the tail and return it.

When the list contains more than one node, we delete the connection between two adjacent nodes. We move to the tail’s previous node, then back to the next node and assign null to it. Next, we move our current tail to its previous node, and finally, we remove the connection between the tail and its previous node by setting temp.previous to null. We then return the last node.

Code

Java
public ListNode deleteLast() {
    if (isEmpty()) {
        throw new NoSuchElementException();
    }
    ListNode temp = tail;
    if (head == tail) {
        head = null;
    } else {
        tail.previous.next = null;
    }
    tail = tail.previous;
    temp.previous = null;
    length--;
    return temp;
}

This method deleteLast is designed to delete the last node of the doubly linked list and return the deleted node. If the list is empty, it throws a NoSuchElementException. Otherwise, it removes the tail node from the list. If the list contains only one node (head and tail are the same), it sets the head to null. Otherwise, it updates the next reference of the previous node of the tail to null. Finally, it updates the tail pointer to the previous node, decrements the length of the list, and returns the deleted node.

Advantages of Doubly Linked Lists

Doubly linked lists offer several advantages over other data structures:

  1. Bidirectional traversal: Unlike singly linked lists, where traversal is only possible in one direction, doubly linked lists allow traversal in both forward and backward directions.
  2. Efficient insertion and deletion: Insertion and deletion operations can be performed more efficiently in doubly linked lists since only the adjacent nodes need to be updated.
  3. Dynamic size: Doubly linked lists can grow or shrink dynamically, allowing for efficient memory utilization.

Conclusion

Doubly linked lists are a versatile data structure that provides efficient operations for dynamic storage and retrieval of data. Understanding their implementation and common operations is essential for any Java programmer. By utilizing the bidirectional traversal and efficient insertion/deletion capabilities, doubly linked lists offer an excellent alternative to arrays or singly linked lists in various programming scenarios.

Circular Linked List

Unlocking the Power of Circular Linked Lists in Java: A Comprehensive Guide

Linked lists are a fundamental data structure in computer science, offering flexibility and efficiency in managing collections of data. Among the variations of linked lists, the circular linked list stands out for its unique structure and applications. In this blog post, we’ll delve into the concept of circular linked lists, explore their implementation in Java, and discuss their advantages and use cases.

Understanding Circular Linked Lists

A circular linked list is similar to a regular linked list, with the key distinction being that the last node points back to the first node, forming a circle. Unlike linear linked lists, which have a NULL pointer at the end, circular linked lists have no NULL pointers within the list itself. This circular structure allows for traversal from any node to any other node within the list.

The basic components of a circular linked list include:

  • Node: A unit of data that contains both the data value and a reference (pointer) to the next node in the sequence.
  • Head: A reference to the first node in the list. In a circular linked list, this node is connected to the last node, forming a circle.
  • Tail: Although not always explicitly maintained, it refers to the last node in the list, which points back to the head.

How to represent a circular singly linked list in java?

A Circular Singly Linked List is similar to a singly linked list, with the difference that in a circular linked list, the last node points to the first node and not null. Instead of keeping track of the head, we keep track of the last node in the circular singly linked list.

For example:

In a Singly Linked List:

head –> 1 –> 8 –> 10 –> 16 –> null

In a Circular Singly Linked List:

Java
last --> 16 --> 1 --> 8 --> 10 --> 
    ^____________________________|

last –> 16 –> 1 –> 8 –> 10 –> 16 // Please refer to the actual diagram for clarification.

We can insert nodes at both the end and beginning with constant time complexity.

How to implement a Circular Singly Linked List in a java ?

Java
public class CircularSinglyLinkedList {

  private ListNode last;        // Keep track of the last node of the circular linked list   
  private int length;           // Hold the size of the circular singly list

  private class ListNode {
    private int data;
    private ListNode next;

    public ListNode(int data) {
       this.data = data;
    }
  }

  public CircularSinglyLinkedList() {
    last = null;       // When we initialize the circular singly linked list, we know the last points to null and that time the list is empty 
    length = 0;        // So the length is also 0;
  }

  // Gives the size of the circular singly linked list 
  public int length() {
    return length;
  }

  // Check whether the circular list is empty or not 
  public boolean isEmpty() {
    return length == 0;
  }


  public void createCircularLinkedList() {
    ListNode first = new ListNode(1);
    ListNode second = new ListNode(5);
    ListNode third = new ListNode(10);
    ListNode fourth = new ListNode(15);
 
    first.next = second;
    second.next = third;
    third.next = fourth;
    fourth.next = first;      // Here we make the list in circular nature by assigning the first node 

    last = fourth;         // Last node points to the fourth node
  }


  public static void main(String[] args) {
     CircularSinglyLinkedList csll = new CircularSinglyLinkedList();
     csll.createCircularLinkedList();
  }
}

This Java class CircularSinglyLinkedList defines a circular singly linked list. It includes inner class ListNode for representing each node in the list. The class provides methods to create a circular linked list, check its length, and check whether it is empty. The main method demonstrates creating a circular linked list instance and initializing it.

How to traverse and print a circular linked list in java ?

Java
last --> 16 --> 1 --> 8 --> 10 --> 
    ^____________________________|

Algorithm & Execution

Java
if (last == null) {
    return;
}
ListNode first = last.next;
while (first != last) {
    System.out.println(first.data + "");
    first = first.next;
}
System.out.println(first.data + "");

The basic idea here is to find the first node using the last node, and then traverse the list from the first node to the last node. When the first node equals the last node, at that point, we are at the last node, but our while loop terminates, so we couldn’t print the last node’s data. That’s why we print it separately after the loop.

Code

Java
public void display() {
   if (last == null) {
      return;
   }
   ListNode first = last.next;
   while (first != last) {
     System.out.println(first.data + "");
     first = first.next;
   }
   System.out.println(first.data + "");
}

This method display is designed to print the elements of the circular singly linked list. It starts from the first node, which is the node after the last node. Then, it traverses the list until it reaches the last node, printing the data of each node. Finally, it prints the data of the last node itself. If the list is empty (i.e., last is null), the method simply returns without performing any operation.

How to insert node at the beginning of a circular Singly Linked List in java ?

Java
ListNode temp = new ListNode(data);
if (last == null) {
    last = temp;
} else {
    temp.next = last.next;
}
last.next = temp;
length++;
  1. We create a temporary node (temp) which we will insert at the beginning of the circular list.
  2. If the last node is null, it means our list is empty. In this case, we assign the temp node to be the last node. Both last and temp now point to the same new node we inserted. We update the last.next pointer to point to temp, forming the circular nature for the first node.
  3. If the list is not empty (last is not null), we create a new temp node with the given data, and it points to null initially. Then, we update temp.next to point to last.next, so it will be added at the beginning of the last node. Even now, last and temp.next will point to the last node. We need to update the last node’s next pointer to temp so that the circular list nature remains intact.
  4. Finally, we increment the length by 1 because every time we add a new node at the beginning of the last node.

Code

Java
public void insertFirst(int data) {
  ListNode temp = new ListNode(data);
  if (last == null) {
      last = temp;
  } else {
      temp.next = last.next;
  }
  last.next = temp;
  length++;
}

This method insertFirst is designed to insert a new node with the given data at the beginning of the circular singly linked list. If the list is empty (i.e., last is null), the new node becomes both the first and last node. Otherwise, the new node is inserted after the last node, and its next reference is updated to point to the node originally after the last node. Finally, the length of the list is incremented.

How to insert node at the end of a circular singly linked list in Java?

Java
ListNode temp = new ListNode(data);
if (last == null) {   // list is empty
   last = temp;
   last.next = last;
} else {              // if list is non-empty
   temp.next = last.next;
   last.next = temp;
   last = temp;
}
length++;
  1. First, we create a new temporary node (temp) with the given data value.
  2. Initially, the circular list is empty, so last will point to null, i.e., last --> null.
  3. When we insert a new node, we check whether we are inserting into an empty list or a non-empty list.
  4. If the list is empty, we point last to our temporary node, i.e., last = temp.
  5. To create a circular structure, we need to make last.next point to last itself, i.e., last.next = last.
  6. After that, we increment the length by 1 as we successfully inserted a new node into the list.
  7. Now, let’s consider the scenario of a non-empty circular list. In this case, first, we assign last.next to the last node itself when there is only one node. If there is more than one node, then last.next will point to the first node, and temp.next will point to null.
  8. Then, we attach the new temporary node to the end by assigning it as last.next. Previously, it pointed to itself because there was only one node. If there are more than one node, it will point to the temp node because the last node becomes the first, and temp will always become the last node to maintain the circular chain nature.
  9. Finally, temp becomes our new last because it is added at the end.

So, the basic logic is to add the temporary node at the end and always make that newly inserted node the last one.

How to remove first node from a circular singly linked list in java ?

Java
if (isEmpty()) {
   throw new NoSuchElementException();
}
ListNode temp = last.next;
if (last.next == last) {
   last = null;
} else {
   last.next = temp.next;
}
temp.next = null;
length--;
return temp;
  1. If the list is empty, meaning there are no elements to remove, we throw a NoSuchElementException.
  2. We create a temporary node temp which will store the node to be removed, i.e., the first node (last.next).
  3. If last.next points to last itself, it means there is only one node in the list. In this case, we remove the last node by assigning null to last.
  4. If last.next doesn’t point to last, it means there are multiple nodes in the list. We remove the first node by updating last.next to point to the second node (temp.next).
  5. We then set temp.next to null to detach temp from the list.
  6. We decrement the length of the list by 1 since we have successfully removed a node.
  7. Finally, we return the removed node temp.

So, the main logic is to remove the first node by adjusting pointers and then returning the removed node.

Advantages of Circular Linked Lists

Circular linked lists offer several advantages over their linear counterparts:

  1. Efficient Insertion and Deletion: Insertion and deletion operations can be performed quickly, especially at the beginning or end of the list, as there’s no need to traverse the entire list.
  2. Circular Traversal: With a circular structure, traversal from any point in the list to any other point becomes straightforward.
  3. Memory Efficiency: Circular linked lists save memory by eliminating the need for a NULL pointer at the end of the list.

Use Cases of Circular Linked Lists

Circular linked lists find applications in various scenarios, including:

  • Round-Robin Scheduling: In operating systems, circular linked lists are used to implement round-robin scheduling algorithms, where tasks are executed in a circular order.
  • Music and Video Playlists: Circular linked lists can be used to implement circular playlists, allowing seamless looping from the last item to the first.
  • Resource Management: In resource allocation systems, circular linked lists can represent a pool of resources where allocation and deallocation operations are frequent.

Conclusion

Circular linked lists provide an elegant solution to certain problems by leveraging their circular structure and efficient operations. In Java, implementing a circular linked list involves managing node connections carefully to maintain the circular property. Understanding the strengths and applications of circular linked lists can aid in designing efficient algorithms and data structures for various computational tasks.

Singly Linked List in Java

Unlocking Singly Linked List Logical Operations in Java: Master Your Skills

Singly linked lists are versatile data structures that offer efficient insertion, deletion, and traversal operations. However, beyond the basic CRUD (Create, Read, Update, Delete) operations, there lies a realm of logical operations that can be performed on singly linked lists. In this detailed blog, we’ll explore some of the most important logical operations that can be applied to singly linked lists in Java, providing insights into their implementation and usage.

How to search an element in a Singly Linked List in Java?

head –> 10 –> 8 –> 1 –> 11 –> null

Java
ListNode current = head;
while(current != null)
{
  if(current.data == searchKey)
  {
    return true;
  }
  current = current.next;
}
return false;

As the main logic, we need to traverse the list node by node. While traversing, we check each node’s data. If it matches the search key, then we’ve found the key; otherwise, we haven’t found it.

  1. We create a temporary node current to traverse the list until the end. Initially, it’s set to the head node, i.e., ListNode current = head.
  2. Using a while loop, we traverse until the end of the list. If current becomes null, it means we’ve reached the end, and we terminate the loop. During traversal, we check each node’s data with the search key. If we find a match, we return true immediately and exit the loop.
Java
while( current  != null )
{
  if(current.data == searchKey)
  {
   return true;
  }
  current = current.next;      // Move to the next node in each iteration 
}
  1. Finally, if we haven’t found the exact search key after traversing the entire list, we return false.

Output:

  • If the search key is 1, then it is found.
  • If the search key is 12, then it is not found.

Code

Java
public boolean find(ListNode head, int searchKey) {
  if (head == null) {
    return false;
  }

  ListNode current = head;
  while (current != null) {
    if (current.data == searchKey) {
      return true;
    }
    current = current.next; // Move to the next node
  }
  return false;
}

This method find is designed to search for a specific key value (searchKey) within the linked list. It returns true if the key is found, and false otherwise. If the list is empty (i.e., head is null), it immediately returns false. Otherwise, it traverses the list, comparing the data value of each node (current.data) with the search key. If a match is found, it returns true. If the end of the list is reached without finding the key, it returns false.

How to reverse a singly linked list in java

Input:

head –> 10 –> 8 –> 1 — > 11 –> null

Output:

head –> 11 –> 1 –> 8 –> 10 –> null

Java
ListNode current = head;
ListNode previous = null;
ListNode next = null;
while(current != null)
{
  next = current.next;
  current.next = previous;
  previous = current;
  current = next;
}
return previous;

The main logic is to traverse the list until the end and apply a logic that reverses the pointing of each node. Ultimately, we obtain the reversed list.

  1. We create three temporary nodes:
    • current points to head.
    • previous initially points to null.
    • next also initially points to null.
  2. We traverse the list node by node using a while loop. The loop iterates until current becomes null.
  3. In each iteration of the while loop, we perform the following operations:
Java
while(current != null)
{
   next = current.next;        // Store the reference to the next node
   current.next = previous;    // Reverse the pointing direction of the current node
   previous = current;         // Move forward: previous becomes current
   current = next;             // Move forward: current becomes next
}
    • First, we move to the next node by assigning next to current.next.
    • Second, we reverse the reference of the current node to point to the previous node.
    • Third, we update previous to be the current node, preparing for the next iteration.
    • Finally, we move forward by assigning next to current.
    This process effectively reverses the pointing direction of each node in the list.
  1. When the while loop terminates, we have reversed the entire list, and the last previous node becomes the new head of the list. So, we return previous.

Output: head --> 11 --> 1 --> 8 --> 10 --> null.

After the reversal, the list becomes reversed.

Code

Java
public ListNode reverse(ListNode head)
{
   if(head == null)
   {
      return head;
   }
   
   ListNode current = head;
   ListNode previous = null;
   ListNode next = null;

   while(current != null)
   {
      next = current.next;
      current.next = previous;
      previous = current;
      current = next;
   }
   return previous;
}

This method reverse is designed to reverse the linked list. If the list is empty (i.e., head is null), it immediately returns null. Otherwise, it iterates through the list, changing the next pointer of each node to point to the previous node. At the end of the iteration, it returns the last node encountered, which becomes the new head of the reversed list.

How to find middle node in singly linked list in java ?

To find the middle node in a singly linked list, we employ the same logic for two different cases.

Case 1: List having an even number of nodes:

For example, head –> 10 –> 8 –> 1 –> 11 –> null

In this case, the middle node is 1.

Case 2: List having an odd number of nodes:

For example, head –> 10 –> 8 –> 1 –> 11 –> 15 –> null

Here again, the middle node is 1.

Java
ListNode slowPtr = head;
ListNode fastPtr = head;
while(fastPtr != null && fastPtr.next != null) {
    slowPtr = slowPtr.next;      // Move slow pointer to the next node
    fastPtr = fastPtr.next.next; // Move fast pointer to two nodes ahead
}
return slowPtr; // Return the slow pointer, which points to the middle node

The main logic involves using two different pointers: a slow pointer and a fast pointer. The slow pointer moves to the next node one by one, while the fast pointer moves two nodes ahead at a time. When the fast pointer reaches the end (either pointing to null or its next points to null), the while loop terminates, and we return the slow pointer, which represents the middle node in both cases.

Code

Java
public ListNode getMiddleNode() {
    if (head == null) {
      return null;
    }
    
    ListNode slowPtr = head;
    ListNode fastPtr = head;
 
    while (fastPtr != null && fastPtr.next != null) {
       slowPtr = slowPtr.next;
       fastPtr = fastPtr.next.next;
    }
    return slowPtr;
}

This method getMiddleNode is designed to find and return the middle node of the linked list. It initializes two pointers, slowPtr and fastPtr, both starting at the head of the list. The slowPtr moves one node at a time while the fastPtr moves two nodes at a time. When the fastPtr reaches the end of the list (or null), the slowPtr will be at the middle node. If the list is empty (i.e., head is null), it returns null.

How to detect a loop in Singly Linked List in java ?

In a given singly linked list, if there exists a loop, it can be identified by employing the following logic.

Consider the linked list: head –> 1 –> 2 –> 3 –> 4 –> 5 –> 6 –> 3

As it can be seen, the list loops back to the node with value 3.

The main logic remains the same as before: we use two different pointers, a slow pointer and a fast pointer. However, in this case, we move the fast pointer first, followed by the slow pointer. Due to the loop, these pointers will eventually meet at the same node. Once the slow and fast pointers are equal, pointing to the same node, we can conclude that there exists a loop in the linked list. If the pointers never meet, then the list does not contain any loop.

Java
ListNode fastPtr = head;
ListNode slowPtr = head;
while(fastPtr != null && fastPtr.next != null) {
    fastPtr = fastPtr.next.next; // Move fast pointer two nodes ahead
    slowPtr = slowPtr.next;      // Move slow pointer one node ahead
    if(slowPtr == fastPtr) {     // If slow pointer meets fast pointer, it indicates a loop
        return true;
    }
}
return false; // If loop termination condition is met without meeting points, return false

The main logic is the same as before: we use two pointers, a slow pointer and a fast pointer, to traverse the list. However, in this case, we move the fast pointer two nodes ahead and the slow pointer one node ahead in each iteration. If the pointers meet at any point during traversal, it indicates the presence of a loop in the list, and we return true. If the loop termination condition is met without the pointers meeting, it means there is no loop in the list, and we return false.

Code

Java
public boolean containsLoop() {
  ListNode fastPtr = head;
  ListNode slowPtr = head;

  while (fastPtr != null && fastPtr.next != null) {
    fastPtr = fastPtr.next.next;        // We need to move fast pointer fast so that it will catch the slow pointer if a loop is present   
    slowPtr = slowPtr.next;
   
    if (slowPtr == fastPtr) {
      return true;
    }
  }
  return false;
}

public void createALoopInLinkedList() {
  ListNode first = new ListNode(1);  
  ListNode second = new ListNode(2);
  ListNode third = new ListNode(3);
  ListNode fourth = new ListNode(4);
  ListNode fifth = new ListNode(5);
  ListNode sixth = new ListNode(6);

  head = first;
  first.next = second;
  second.next = third;
  third.next = fourth;
  fourth.next = fifth;
  fifth.next = sixth;
  sixth.next = third;
}

The method containsLoop checks whether a loop exists in the linked list using Floyd’s Cycle Detection Algorithm. It initializes two pointers, fastPtr and slowPtr, both starting at the head of the list. The fastPtr moves twice as fast as the slowPtr. If there is a loop in the list, eventually, the fastPtr will catch up with the slowPtr. If no loop is found, the method returns false.

The method createALoopInLinkedList is a helper method to create a loop in the linked list for testing purposes. It creates a linked list with six nodes and then creates a loop by making the next reference of the last node point to the third node.

How to find nth node from the end of a Singly Linked List in java?

Consider the singly linked list:

head –> 10 –> 8 –> 1 –> 11 –> 15 –> null

If we want to find the node that is “n” positions from the end of the list, where “n” is given as 2, then the node containing 11 would be that node.

Java
ListNode mainPtr = head;  // It will move forward when the reference pointer covers the nth position forward from the head 
ListNode referencePtr = head;  // It will move twice: first, it covers the nth distance from the head, then it goes till the end with mainPtr, so that mainPtr will reach the exact position
int count = 0;   // It is to track the number of nodes the reference pointer moved forward
while(count < n) {
    refPtr = refPtr.next;
    count++;
}
while(refPtr != null) {
    refPtr = refPtr.next;
    mainPtr = mainPtr.next;
}

return mainPtr;

The main logic involves using two pointers: a main pointer and a reference pointer. The reference pointer moves forward until it reaches the nth position from the head, while the main pointer remains stationary. After reaching the nth position, the reference pointer continues moving until it reaches the end of the list, while the main pointer moves along with it. When the reference pointer reaches the end of the list, the main pointer will be pointing to the nth node from the end of the list.

Code

Java
public ListNode getNthNodeFromEnd(int n) {

 if (head == null) {
    return null;
 }
 
 if (n <= 0) {
     throw new IllegalArgumentException("Invalid value: n = " + n);
 }

 ListNode mainPtr = head;  // It will move forward when the reference pointer covers the nth position forward from the head  
 ListNode refPtr = head;   // It will move twice: first, it covers nth distance from head, then it goes till the end with mainPtr, so that mainPtr will reach the exact position.

 int count = 0;   // It is to track the number of nodes refPtr moved forward

 while (count < n) {
  if (refPtr == null) {
      throw new IllegalArgumentException(n + " is greater than the number of nodes in the list");
  }

  refPtr = refPtr.next;
  count++;
 }

 while (refPtr != null) {
  refPtr = refPtr.next;
  mainPtr = mainPtr.next;
 }

 return mainPtr;    // The returned mainPtr will be at the nth position from the end of the list
}

This method getNthNodeFromEnd is designed to find and return the nth node from the end of the linked list. It initializes two pointers, mainPtr and refPtr, both starting at the head of the list. The refPtr moves forward n positions from the head. Then, both pointers move forward simultaneously until the refPtr reaches the end of the list. At this point, the mainPtr will be at the nth node from the end. If the list is empty or if the value of n is less than or equal to 0, the method throws an IllegalArgumentException.

How to remove duplicates from sorted Singly Linked List in java?

For the given input of a sorted linked list:

head –> 1 –> 1 –> 2 –> 3 –> 3 –> null

The desired output is a sorted linked list with duplicates removed:

head –> 1 –> 2 –> 3 –> null

Java
ListNode current = head;
while(current != null && current.next != null) {
    if(current.data == current.next.data) {
        current.next = current.next.next; // Connect current to the next next node to remove the duplicate node
    } else {
        current = current.next; // Move to the next node if no duplicate is found
    }
}

The main logic involves traversing the list node by node using a current pointer. While traversing, we check whether the data of the current node is equal to the data of the next node. If they are equal, it means a duplicate node is found, so we connect the current node to the next next node, effectively removing the duplicate node between them. If no duplicate is found, we simply move to the next node. This process continues until we reach the end of the list or the current node becomes null.

Code

Java
public void removeDuplicates() {
  
  if (head == null) {
    return;
  }

  ListNode current = head;

  while (current != null && current.next != null) {
     if (current.data == current.next.data) {
       current.next = current.next.next;
     } else {
       current = current.next;
     }
  }
}

This method removeDuplicates is designed to remove duplicates from a sorted linked list. It iterates through the list using the current pointer. If the current node’s data is equal to the data of the next node, it skips the next node by updating the next reference of the current node to skip the duplicate node. Otherwise, it moves the current pointer to the next node in the list. If the list is empty (i.e., head is null), the method returns without performing any operation.

Now, How to insert a node in a sorted Singly Linked List in java?

Given the sorted linked list:

head –> 1 –> 8 –> 10 –> 16 –> null

And a new node:

newNode –> 11 –> null

We want to insert the new node (11) into the sorted list such that the sorting order remains the same.

After insertion, the updated list would be:

head –> 1 –> 8 –> 10 –> 11 –> 16 –> null

Java
ListNode current = head;
ListNode previous = null;
while(current != null && current.data < newNode.data) {
    previous = current;
    current = current.next;
}
// When we reach the insertion point, we have references to current, previous, and newNode.
// Now, we rearrange the pointers so that previous points to newNode and newNode points to current.
newNode.next = current;
if (previous != null) {
    previous.next = newNode;
} else {
    // If previous is null, it means the newNode should become the new head.
    head = newNode;
}
return head;

The main logic involves traversing the sorted linked list until we find the appropriate position to insert the new node while maintaining the sorting order. We traverse the list node by node, comparing the data of each node with the data of the new node. We continue this process until we find a node whose data is greater than or equal to the data of the new node, or until we reach the end of the list.

When we reach the insertion point, we have references to three nodes: the current node, the previous node (the node before the insertion point), and the new node. To insert the new node into the list, we rearrange the pointers so that the previous node points to the new node, and the new node points to the current node.

If the previous node is null, it means that the new node should become the new head of the list. In this case, we update the head pointer to point to the new node.

Code

Java
public ListNode insertInSortedList(int value) {
  ListNode newNode = new ListNode(value);
 
  if (head == null) {
    return newNode;
  }

  ListNode current = head;
  ListNode previous = null;

  while (current != null && current.data < newNode.data) {   // Will go till the end while checking the sorting order between the current node and the new node data 
     previous = current;
     current = current.next;
  }
  
  // When we reach the insertion point, we have our current, previous, and newNode references so we only need to arrange pointers.
  // So that previous will point to newNode and newNode will point to current.
 
  newNode.next = current;
  if (previous == null) { // If the new node is to be inserted at the beginning
    head = newNode;
  } else {
    previous.next = newNode;
  }
  
  return head;    
}

This method insertInSortedList is designed to insert a new node with the provided value into a sorted linked list. If the list is empty (i.e., head is null), the new node becomes the head of the list. Otherwise, it traverses the list to find the correct position to insert the new node while maintaining the sorted order. Once the insertion point is found, it updates the pointers to insert the new node. Finally, it returns the head of the list.

How to remove a given key from singly linked list in java?

Given the linked list:

head –> 1 –> 8 –> 10 –> 11 –> 16 –> null

Suppose our key is 11, and we want to remove it from the list.

After removal, the updated list would be:

head –> 1 –> 8 –> 10 –> 16 –> null

Java
ListNode current = head;
ListNode previous = null;

// Traverse the list to find the node with the key value
while(current != null && current.data != key) {
   previous = current;
   current = current.next;
}

// If we reached the end of the list without finding the key, return
if(current == null) {
  return;
}

// If we found the key, remove the node by adjusting the previous node's next reference
if(previous != null) {
  previous.next = current.next;
} else {
  // If the key is found at the head, update the head pointer to skip the current node
  head = current.next;
}

The main logic involves traversing the linked list until we find the node with the specified key value. While traversing, we keep track of the previous node as well.

If we reach the end of the list without finding the key, it means the key doesn’t exist in the list, so we return without performing any removal.

If we find the node with the key value, we remove it from the list by adjusting the next reference of the previous node to skip over the current node. However, if the key is found at the head of the list, we update the head pointer to skip over the current node.

Code

Java
public void deleteNode(int key) {
 
  ListNode current = head;
  ListNode previous = null;

  // If we find our key at the first node that is head, just update head to point to the next node.
  if (current != null && current.data == key) {
     head = current.next;
     return;
  }

  while (current != null && current.data != key) {
    previous = current;
    current = current.next;
  }
 
  // If we reached the end of the list (current == null), the key was not found, so return without performing any operation.
  if (current == null) {
    return;
  }

  // If we found the key, update the next reference of the previous node to point to the next node of the current node, effectively removing the current node.
  previous.next = current.next;
}

This method deleteNode is designed to delete a node with the given key value from the linked list. It iterates through the list using the current pointer to find the node with the specified key value while keeping track of the previous node using the previous pointer. If the key is found at the first node (head), it updates the head to point to the next node. If the key is found in the middle of the list, it updates the next reference of the previous node to skip the current node. If the key is not found in the list, the method simply returns without performing any operation.

Bonus: Two Sum Problem in java

Problem: Given an array of integers, return the indices of the two numbers such that they add up to a specific target.

Example: Given array of integers: {2, 11, 5, 10, 7, 8}, and target = 9.

Solution: Since arr[0] + arr[4] = 2 + 7 = 9, we return {0, 4} as the indices.

The main logic involves using a map for fast lookup of stored values to find the exact sum of the target. We require one map for lookup purposes and one result array to store the indices of the two numbers that add up to the target sum from the given array. Here’s how it works:

  1. We iterate through the array, examining each element.
  2. At each element, we calculate the difference between the target and the current element.
  3. We check if this difference exists in the map. If it does, it means we have found the two numbers that add up to the target.
  4. We return the indices of the current element and the element with the required difference.
  5. If the difference is not found in the map, we store the current element’s value along with its index in the map for future lookups.

Algorithm & Executions

Java
int[] result = new int[2];
Map<Integer, Integer> map = new HashMap<>();

for(int i = 0; i < numbers.length; i++) {
    int complement = target - numbers[i];
    if(map.containsKey(complement)) {
        result[0] = map.get(complement);
        result[1] = i;
        return result;
    }
    map.put(numbers[i], i);
}

return result;

The main logic involves using a hash map to store the indices of elements in the array. We traverse the array and, for each element, check if its complement (target – current number) exists in the hash map. If it does, it means we have found two numbers that add up to the target, so we return their indices. If not, we add the current number and its index to the hash map for future reference.

Code

Java
public static int[] twoSum(int[] numbers, int target) {
  int[] result = new int[2];
  Map<Integer, Integer> map = new HashMap<>();
 
  for (int i = 0; i < numbers.length; i++) {
    if (!map.containsKey(target - numbers[i])) {
      map.put(numbers[i], i);
    } else {
      result[1] = i;
      result[0] = map.get(target - numbers[i]);
      return result;
    }     
  }

  throw new IllegalArgumentException("Two numbers not found");
}

This method twoSum is designed to find and return the indices of the two numbers in the numbers array that add up to the target value. It utilizes a hashmap to store the difference between the target and each element of the array along with its index. It iterates through the array, checking if the hashmap contains the difference between the target and the current element. If not, it adds the current element and its index to the hashmap. If it finds the difference in the hashmap, it retrieves the index of the other number and returns the indices as the result. If no such pair of numbers is found, it throws an IllegalArgumentException.

Note: Why is this Array Manipulation problem being discussed here?

While the problem of finding two numbers in an array that add up to a specific target is not directly related to singly linked list operations, the underlying logic and problem-solving techniques used in solving array manipulation problems can indeed be helpful in solving problems related to singly linked lists.

Many problem-solving techniques and algorithms used in array manipulation, such as iterating over elements, using hash maps for fast lookups, or employing two-pointer approaches, can also be applied to singly linked list problems. Additionally, understanding how to efficiently manipulate data structures and analyze patterns in data is a fundamental skill that can be transferred across various problem domains.

In the context of computer science and algorithmic problem-solving, building a strong foundation in problem-solving techniques through various types of problems, including those involving arrays, linked lists, trees, graphs, and more, can enhance your ability to tackle a wide range of problems effectively.

So, while the specific problem discussed may not directly relate to singly linked lists, the problem-solving skills and techniques learned from array manipulation problems can certainly be beneficial in solving problems related to singly linked lists and other data structures.

Conclusion

By mastering the logical operations of singly linked list in Java, programmers can unlock the full potential of this versatile data structure. Whether it’s searching, reversing, merging, or detecting loops, understanding these operations equips developers with powerful tools for solving complex problems and building efficient algorithms. With the insights provided in this blog, programmers can elevate their skills and become more proficient in leveraging singly linked lists for various applications.

SinglyLinkedList and It's Logical Operations

Mastering Singly Linked Lists: Essential Basic Insertion and Deletion Operations Explained

Singly linked lists are a fundamental data structure in computer science, offering dynamic flexibility and memory efficiency. In Java, understanding and mastering them is key to unlocking efficient algorithms and problem-solving skills. This blog aims to guide you from basic concepts to advanced techniques, transforming you from a linked list novice to a ninja!

Anatomy of a Singly Linked List

Each node in a singly linked list comprises two parts: data and a reference to the next node. This pointer-based structure enables dynamic growth and memory management. The first node is called the head, and the last node has a null reference as its next node.

Anatomy of a Singly Linked List

In a singly linked list, each node points to the next node in the sequence, forming a linear chain. The last node typically points to a null reference, indicating the end of the list.

How to represent a Linked List in Java ?

A linked list is a fundamental data structure employed for the storage of a series of elements, objects, or nodes, characterized by the following attributes:

  1. Sequential arrangement of nodes.
  2. Each node comprises data along with a pointer to the subsequent node.
  3. The initial node serves as the head node.
  4. The final node contains data and points to null.
Java
| data | next | ===> | data | next | ===> | data | next | ===> null

Visualizing Nodes: Unraveling the Building Blocks

In the intricate world of linked lists, nodes take center stage, acting as the fundamental units that store data and guide traversals. Let’s dissect the visual representation you provided:

Node Structure:

Java
| data | next |===>
  • data: This field holds the actual value, serving as the heart of the information a node carries. Its contents can vary depending on the application, from simple integers to complex objects.
  • next: This crucial pointer acts as the bridge to the next node in the sequence. By following these references, we can navigate the entire linked list, one node at a time. The arrow -----> represents this connection, visually depicting the flow from one node to the next.

Linked List Illustration

Java
head ====> |10|===>|8|===>|9|===>|11|===> null
  • head: The special node marking the beginning of the linked list. Think of it as the entrance gate, providing the entry point for accessing the list’s elements.
  • Nodes: Each rectangular box, labeled with |data|====>|next|, represents a node in the chain. The values inside the data field indicate the stored values (10, 8, 9, 11).
  • null: This unique value, often denoted by null, signifies the end of the list. Just like reaching the end of a road, encountering null indicates there are no more nodes to follow.
Java
public class SinglyLinkedList {

  private ListNode head; // head node to hold the list
  
  // It contains a static inner class ListNode
  private static class ListNode {
     private int data;
     private ListNode next;

     public ListNode(int data) {
          this.data = data;
          this.next = null;
     }
  }

  public static void main(String[] args) {
   
  }
}

This Java code defines a class named SinglyLinkedList with a private member variable head, which represents the starting node of the linked list. Inside this class, there is a static inner class named ListNode, which encapsulates the data and reference to the next node. The constructor of ListNode initializes the data and sets the next node reference to null.

The main method serves as the entry point, although it’s currently devoid of content. We will proceed to furnish it with creation code shortly.

How to create a singly linked list in java ?

Java
head ---> 10 ---> 8 ---> 1 ---> 11 ---> null

Now, let’s see how we created it:

  1. Initially, the head node points to null because the linked list is empty, i.e., head —> null.
  2. Let’s create the first node with data 10, which also points to null, i.e., 10 —> null.
  3. To hold nodes one by one, the head points to the node with 10. At this stage, the our list contains one node, i.e., head —> 10 —>.
  4. Let’s create the second node with data 8 and next pointing to null, i.e., 8 —> null.
  5. Connect the head node to the second node so that the list will contain 2 nodes, i.e., head –> 10 –> 8 –> null.
  6. Create the third node with data 1 and next pointing to null, i.e., 1 –> null.
  7. Here, we connect the third node to the second node so that the list contains 3 nodes, i.e., head –> 10 –> 8 –> 1 –> null.
  8. Finally, create the fourth node with data 11 and next pointing to null, i.e., 11 –> null.
  9. Connect this fourth node with the third node. Now, the linked list size is 4, i.e., head –> 10 –> 8 –> 1 –> 11 –> null.

Lets write code in above SinglyLinkedList empty main method.

Java
// Let's create a linked list demonstrated in the comment below

// 10 --> 8 --> 1 --> 11 --> null
// 10 as head node of linked list

ListNode head = new ListNode(10);
ListNode second = new ListNode(8);
ListNode third = new ListNode(1);
ListNode fourth = new ListNode(11);

// Attach them together to form a list 

head.next = second;      // 10 --> 8
second.next = third;     // 10 --> 8 --> 1
third.next = fourth;     // 10 --> 8 --> 1 --> 11 --> null

This code snippet demonstrates the creation of a linked list with four nodes, where each node contains an integer value. The comments above describe the structure of the linked list being created, and the subsequent lines of code link the nodes together to form the desired sequence.

Final code look like:

Java
public class SinglyLinkedList {

    private ListNode head; // head node to hold the list

    // It contains a static inner class ListNode
    private static class ListNode {
        private int data;
        private ListNode next;

        public ListNode(int data) {
            this.data = data;
            this.next = null;
        }
    }

    public static void main(String[] args) {

        // 10 --> 8 --> 1 --> 11 --> null
        // 10 as head node of linked list

        ListNode head = new ListNode(10);
        ListNode second = new ListNode(8);
        ListNode third = new ListNode(1);
        ListNode fourth = new ListNode(11);

        // Attach them together to form a list 

        head.next = second; // 10 --> 8
        second.next = third; // 10 --> 8 --> 1
        third.next = fourth; // 10 --> 8 --> 1 --> 11 --> null

    }
}

How to print elements of Singly Linked List in java

As we know, to print all elements in a linked list, we need to traverse through all nodes in the linked list until the end of the linked list.

So,

head –> 10 –> 8 –> 1 –> 11 –> null

Algorithm & Execution

Java
current = head;
while(current != null)
{
    Sop(current.data);
    current = current.next;
}
  1. Let’s create a current node which initially points to null, i.e., current –> null. At this point, the output is null.
  2. Now we will assign the head node of the linked list to this current node, i.e., current = head.
  3. To traverse the list node by node until the end of the list, we will use a while loop which checks whether the current node is null. If it is null, that means we reached the end of the list.
  4. So, for every iteration, we will print that particular node’s data and also shift the current node pointer to the next node so that we traverse to the next node.

Code

Java
// Given a ListNode, prints all elements it hold 
public void display(ListNode head)
{
  if(head == null)
  {
    return;
  }

  ListNode current = head;
  
 // loop each element till end of the list 
 // last node points to null

  while(current != null)
  {
    System.out.println(current.data + " --> ");   // prints current element's data
    // move to next element
    current = current.next;
  }
  System.out.print(current);  // here current wil be null 
  
  
  

Run with -->

SinglyLinkedList singlyLinkedList = new SinglyLinkedList();
singlyLinkedList.display(head);


o/p : 10 --> 8 --> 1 --> 11 --> null

This code snippet defines a method display within the SinglyLinkedList class, responsible for printing all elements of the linked list starting from the provided head node. It iterates through the linked list nodes, printing each node’s data, and ends with printing “null” once the end of the list is reached. Finally, the method is invoked with a head node, displaying the elements of the linked list.

How to find the length of singly linked list in java?

Let’s determine the length of the linked list programmatically

Head –> 10 –> 8 –> 1 –> 11 –> null

Algorithm & Execution

Java
current = head;
int count = 0;
while(current != null)
{
  count++;
  current = current.next;
}
  1. We will create a temporary node called current, initially pointing to null.
  2. The current node is set to point to the head of the linked list.
  3. We create a count variable to keep track of the number of nodes present in the list, initialized as int count = 0.
  4. Execute the while loop until the current node reaches the end of the list. We check if the current node equals null; if yes, it means we’ve reached the end. For each iteration, we increment the count variable by 1, and the current node pointer moves to the next node.

Code

Java
// Given a ListNode head, find out the length of the linked list 
public int length(ListNode head) {
  if (head == null) {
    return 0;
  }

  // Create a count variable to hold the length
  int count = 0;

  // Loop through each element and increment the count until the list ends 
  ListNode current = head;
  whi le (current != null) {
    count++;
    // Move to the next node 
    current = current.next;
  }
  
  return count;
}

Here, code snippet defines a method length within the SinglyLinkedList class, which calculates the length of the linked list starting from the provided head node. It iterates through the linked list nodes, incrementing a count variable for each node encountered, until it reaches the end of the list. Finally, it returns the count representing the length of the linked list.


Singly Linked List Insertion Operation

One of the primary operations performed on singly linked lists is insertion, which involves adding new nodes at various positions within the list. Let’s explore their complexities, implementation techniques, and best practices.

How to inser node at the beginning of singly linked list in java

Inserting a new node at the beginning of a singly linked list is a straightforward operation. It involves creating a new node with the desired data value and adjusting the pointers to ensure proper linkage.

Head –> 10 –> 8 –> 1 –> 11 –> null

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
newNode.next = head;
head = newNode;
  1. The first step creates a new node with data 15 and next pointing to null, i.e., newNode –> 15 –> null.
  2. To insert the new node at the beginning of the list, update the next pointer of the new node to point to the current head node. This means connecting the new node’s next reference to the head node of the list, i.e., newNode –> 15 –> head.
  3. The final step updates the head pointer to point to the new node, making the new node the head node of the linked list.After execution, the linked list becomes: head –> 15 –> 10 –> 8 –> 1 –> 11 –> null.”

Code

Java
public ListNode insertAtBeginning(ListNode head, int data) {
  ListNode newNode = new ListNode(data);
  if (head == null) {
    return newNode;
  }
  newNode.next = head;
  head = newNode;
  return head;   // this head will be the new head, having the new node at the beginning  
}

This method insertAtBeginning is designed to insert a new node with the provided data at the beginning of the linked list. If the list is initially empty (i.e., head is null), the new node becomes the head of the list. Otherwise, the new node is inserted before the current head, and the head pointer is updated to point to the new node. Finally, the updated head is returned.

How to insert a node at the end of singly linked list in java?

Inserting a new node at the end of a singly linked list requires traversing the list until reaching the last node, then updating the last node’s reference to point to the new node.

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
ListNode current = head;
while(null != current.next)
{
 current = current.next;
}
current.next = newNode;
  1. Creates a new node with data 15 and next pointing to null, i.e., newNode –> 15 –> null.
  2. To insert the new node at the end of the list, we need to traverse the list till the end and then assign the new node to the last node’s next pointer. For this, we create a temporary node called current, which initially points to null. Then we make this current node point to the head of the list, i.e., ListNode current = head.
  3. We use a while loop to reach the last node of the list. For each iteration, we check whether the current node’s next pointer points to null or not. Also, for each iteration, we update current to its next pointer. When we reach the last node, we stop and terminate the while loop.
Java
while(current.next != null)
{
  current = current.next;
}

Finally, we assign the new node to current.next pointer because at this point, current is the last node of the list.

Code

Java
public ListNode insertAtEnd(ListNode head, int data) {
  ListNode newNode = new ListNode(data);
  if (head == null) {
    return newNode;
  }
  ListNode current = head;
  // Loop until the last node of the list (Note: it's not until the end of the list; in this case, current == null. Here, current.next being null means we're at the last node of the list.)
  while (current.next != null) {
    current = current.next;  // Move to the next node 
  }
  current.next = newNode;    // Assign the new node to the last node 
  return head;
}

This method insertAtEnd is designed to insert a new node with the provided data at the end of the linked list. If the list is initially empty (i.e., head is null), the new node becomes the head of the list. Otherwise, it traverses the list until it reaches the last node. Then, it assigns the new node as the next node of the last node and returns the head of the list.

How to insert a node in singly linked list after a given node in java ?

In this scenario, the initial step involves traversing the linked list until the preceding node of the insertion point is reached. Subsequently, node pointers are adjusted accordingly.

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
newNode.next = previous.next;
previous.next = newNode;
  1. Creates a new node with data 15 and its next pointer pointing to null.
  2. Since we’re inserting the new node between the specified previous node and its next node, we first assign the previous node’s next pointer to the new node’s next pointer. This means connecting the new node’s next pointer to the node that was originally after the previous node, i.e., newNode –> 15 –> 11 –> null.
  3. Finally, we update the previous node’s next pointer to point to the new node, so that the new node is placed between the specified previous node and its next node.After execution, the linked list becomes: head –> 10 –> 8 –> 15 –> 11 –> null.

Code

Java
public void insertAfter(ListNode previous, int data) {
  if (previous == null) {
    System.out.println("The given previous node cannot be null.");
    return;
  }

  ListNode newNode = new ListNode(data);
  newNode.next = previous.next;
  previous.next = newNode;
}

This method insertAfter is designed to insert a new node with the provided data after a specified node in the linked list. It first checks if the given previous node is not null. If it is null, it prints an error message and returns. Otherwise, it creates a new node with the provided data, links it to the node following the specified previous node, and then updates the next reference of the previous node to point to the new node.

How to insert a node in singly linked list at a given position in java?

Inserting a node at a specific position in a singly linked list involves traversing the list to find the desired position, then adjusting the pointers accordingly.

head –> 10 –> 8 –> 11 –> null

Given a linked list with three nodes, where the first node contains the value 10 and points to position 1, the second node contains the value 8 and points to position 2, we need to insert a new node at position 3. Therefore, to insert the new node at position 3, we must traverse the linked list until we reach position 2 and then insert the new node after position 2.

Algorithm & Execution

Java
ListNode newNode = new ListNode(15);
ListNode previous = head;
int count = 1;
while(count < position - 1)
{
  previous = previous.next;
  count++;
}
ListNode current = previous.next;
newNode.next = current;
previous.next = newNode;
  1. Creates a new node with data 15 and its next pointer pointing to null, i.e., 15 –> null.
  2. To insert the new node at the given position, we need to traverse until position – 1.
  3. We create a temporary node named previous and point it to the head of the list, i.e., previous –> head.
  4. To track the number of nodes traversed, we create a count variable and initialize it to 1, because the first node has already been traversed by the previous node, i.e., int count = 1.
  5. Let’s execute a few steps within the while loop. We traverse until just before the given position node, checking how many nodes we’ve traversed using the while loop until position – 1.
Java
while(count < position - 1)   // When we reach position - 1, the loop will terminate (when count = 2), and at this point, our 'previous' will point to position - 1.
{
  previous = previous.next;
  count++;    // As 'previous' moves forward, we increment count by one.
}
  1. Moving ahead, we create a temporary node named current, which will hold the next node after ‘previous’, i.e., current –> 11, and previous –> 8.
  2. The next pointer of the new node will point to the ‘current’ node, establishing a new connection between the new node and the ‘current’ node, i.e., newNode –> 15 –> 11.
  3. The final step is to set the next pointer of ‘previous’ to point to the new node, i.e., previous –> 8 –> newNode –> 15.

Output: head –> 10 –> 8 –> 15 –> 11 –> null. So, we inserted 15 at position 3, where ‘previous’ –> 8 and ‘current’ –> 11, and newNode –> 15 will be between the ‘previous’ and ‘current’ node.

Code

Java
public ListNode insertAtPosition(ListNode head, int data, int position) {
   // Perform boundary checks 
   int size = length(head);      // We use the already defined length method which returns the size of the list
   if (position > size + 1 || position < 1) {   // If the position is greater than the number of nodes in the list or less than 1, then it is an invalid position 
     System.out.println("Invalid position");
     return head;
   }

   ListNode newNode = new ListNode(data);
   if (position == 1) {                     // If position == 1, it means the list contains only one node and we want to insert at that position. Then we assign newNode next to head and return newNode as the new head of the list 
     newNode.next = head;
     return newNode;
   }
   else {
     // Else we perform our regular algorithm 
    ListNode previous = head;
    int count = 1;
    while (count < position - 1) {
       previous = previous.next;
       count++;
    }

    ListNode current = previous.next;
    newNode.next = current;
    previous.next = newNode;
    return head;
   }
 }
}

This method insertAtPosition is designed to insert a new node with the provided data at a specified position in the linked list. It first performs boundary checks to ensure that the position is valid. If the position is valid, it creates a new node with the provided data. If the position is 1, indicating that the node needs to be inserted at the beginning of the list, it updates the new node’s next reference to the current head and returns the new node as the new head of the list. Otherwise, it traverses the list to the node just before the specified position, updates the next reference of the new node to the current node at the specified position, and updates the next reference of the previous node to the new node. Finally, it returns the head of the list.


Singly Linked List Deletion Operation

Deletion operations play a crucial role in managing linked lists efficiently. In this section, we’ll delve into the intricacies of deleting nodes from singly linked lists, exploring various scenarios, implementation techniques, and best practices.

How to delete first node from a singly linked list in java?

Deleting a node from the beginning of a singly linked list is a straightforward operation. It involves updating the reference of the head node to point to the next node in the list.

head –> 10 –> 8 –> 1 –> 11 –> null;

Algorithm & Execution

Java
ListNode temp = head;
head = head.next;
temp.next = null;
  1. Let’s create a temporary node named temp and point it to head, i.e., ListNode temp = head.
  2. In order to delete the first node from the list, we need to remove the reference to the first node from the head pointer. Thus, we update the head pointer to point to the next node in the list, i.e., head = head.next.
  3. We disconnect the first node from the linked list by assigning null to the temp node’s next pointer, i.e., temp.next = null.

Output: head --> 8 --> 1 --> 11 --> null.

Initially, the list has 5 nodes. After deleting the first node, only three nodes remain.

Code

Java
public ListNode deleteFirst(ListNode head) {
   if (head == null) {
      return head;
   }

   ListNode temp = head;
   head = head.next;
   temp.next = null;
   return temp;
}

This method deleteFirst is designed to delete the first node of the linked list. If the list is empty (i.e., head is null), it returns null. Otherwise, it stores the reference to the current head node in a temporary variable temp, updates the head to the next node in the list, sets the next reference of the original head to null, and then returns the deleted node.

How to delete last node of singly linked list in java?

Deleting a node from the end of a singly linked list requires traversing the list until reaching the second-to-last node, then updating its reference to NULL.

head –> 10 –> 8 –> 11 –> null

Algorithm & Execution

Java
ListNode last = head;
ListNode previousToLast = null;
while(last.next != null)
{
  previousToLast = last;
  last = last.next;
}
previousToLast.next = null; // Disconnect the last node from the list
  1. First, we create a temporary node called last, which initially points to the head node, i.e., ListNode last = head.
  2. We create another temporary node called previousToLast and assign null to it.
  3. We traverse until the last node’s next pointer points to null, so that last becomes the last node of the list and previousToLast points to the second last node of the list.
Java
while(last.next != null)
{
   previousToLast = last;  // Becoming the previous node of the current last node 
   last = last.next;       // Move to the next node to become the last node for every iteration.
}
  1. In the final step, we disconnect the second last node from the last node by assigning null to its next pointer, i.e., previousToLast.next = null.
  2. Optionally, you can return the deleted last node if such a requirement exists, for example, for printing purposes.

Code

Java
public ListNode deleteLast(ListNode head) {
   if (head == null) {
       return head;
   }

   ListNode last = head;
   ListNode previousToLast = null;Singly 
  
   while (last.next != null) {
     previousToLast = last;
     last = last.next;
   }

   previousToLast.next = null;
   return last;
}

This method deleteLast is designed to delete the last node of the linked list. If the list is empty (i.e., head is null), it returns null. Otherwise, it traverses the list to find the last node (last) and the node just before it (previousToLast). It then updates the next reference of previousToLast to null, effectively removing the last node from the list. Finally, it returns the deleted node.

How to delete a node from Singly Linked List at a given position in Java?

Deleting a specific node from a singly linked list involves finding the node to be deleted and adjusting the pointers of its neighboring nodes to bypass it.

head –> 10 –> 8 –> 15 –> 11 –> null

Algorithm & Execution

Java
Listwhile(count < position - 1)
{
  previous = previous.next;
  count++;
}
Node previous = head;
int count = 1;
while(count < position - 1)
{
  previous = previous.next;
  count++;
}

ListNode current = previous.next;
previous.next = current.next;
current.next = null;
  1. We create a temporary node called previous, which initially points to the head.
  2. To keep track of the number of nodes we traverse, we use a count variable. Initially, it’s 1 because the first node has already been traversed by the previous node, which points to 10.
  3. We traverse until the node before the position we want to delete. This means we stop at position – 1.
Java
while(count < position - 1)
{
  previous = previous.next;
  count++;
}
  1. When the while loop terminates, we are at position – 1, which is the node before the node we want to delete.
  2. We create a temporary node called current, which holds the reference of the node to be deleted.
  3. To delete the node, we make previous.next point to current.next, effectively bypassing the current node in the linked list.
  4. Finally, we set current.next to null to disconnect it from the list.

Output: head --> 10 --> 8 --> 11 --> null.

As a result of deleting the node at position 3, which contains 15, the linked list becomes head --> 10 --> 8 --> 11 --> null.

Code

Java
public ListNode deleteAtPosition(ListNode head, int position) {
  int size = length(head);  // Used the already defined method to find out the size of the list 
  if (position > size || position < 1) {
     System.out.println("Invalid position");
  }

  if (position == 1) {     
    // If position is 1, it means we will apply delete first node logic 
    ListNode temp = head;
    head = head.next;
    temp.next = null;
    return temp;
  }
  else {
    ListNode previous = head;
    int count = 1;
    while (count < position - 1) {
      previous = previous.next;
      count++;
    }

    ListNode current = previous.next;
    previous.next = current.next;
    current.next = null;
   
    return current;
  }
}

This method deleteAtPosition is designed to delete a node at the specified position in the linked list. It first finds the size of the list using the already defined length method and performs a boundary check to ensure that the position is valid. If the position is invalid, it prints an error message. If the position is 1, indicating that the first node needs to be deleted, it applies the logic of deleting the first node. Otherwise, it traverses the list to find the node just before the specified position (previous). It then updates the next reference of previous to skip the node to be deleted and returns the deleted node.

Conclusion

By mastering singly linked lists in Java, you’ll not only enhance your understanding of fundamental data structures but also improve your problem-solving skills and become a more proficient programmer. With the comprehensive knowledge provided in this guide, you’ll be well-equipped to tackle a wide range of programming challenges and develop efficient and elegant solutions.

error: Content is protected !!