Thursday, 16 January 2025

How to get "i" th bit of a number in Python

 Background

In various problem-solving techniques, we often need to find the i'th bit of a number. For eg., lets say we have the number 5 , it's binary notation is 101. If I want to get 0th bit it is 1, 1st bit is 0 and 2nd bit is again 1. In this post, we will see how to get the ith bit for a given number in Python.


Use of getting i'th bit

Before we see how to get ith bit let's try to understand a problem-solving use case where we can use this logic. Consider the following problem statement:

Given an array of integers, "data" , find all possible subsets of data, including the empty set.

The question is basically asking us to find all possible subsets of a given array of integers.
For e.g, consider input as [2, 5, 7] the possible combinations are
  • i=0: {}
  • i=1: {2}
  • i=2: {5}
  • i=3: {2,5}
  • i=4: {7}
  • i=5: {2,7}
  • i=6: {5,7}
  • i=7: {2,5,7}
The total combinations possible for a given list is 2^N where N is the length of the array. In the above case since N=3 (length of input array) the expected combinations are 2^3 = 8 which is exactly what you see above.

Now to compute these we iterate from i=0 to 2^N-1 which in this case will be from 0 to 7.
For each of these numbers, we can find the binary notation and then check if the binary bit is 1 for a given index and include it in the combination.

For eg., let's take i=3, binary notation is 110 which means we should consider numbers from 1st and 2nd position of the original array. Since the original array was [2,5,7] and we are considering 1st and 2nd position arrays the subset is {2,5} which is what you see in above combination list as well.

For all "i" values subsets computed are shown below:



Now let's see the Python code to get the ith bit for a number.

NOTE: In python for computing 2^N we do 2 ** N, In Java you would do Math.pow(2, N).

How to get "i" th bit of a number in Python

We will test the same example we took above when num =3 and we want to get all bits of it to decide which position data from the original array to include in the current subset. Python code for the above is as follows:
def get_ith_bit(num, i):
    # Shift the operand specified number of bits to the left
    temp = (1 << i)
    temp = temp & num
    if temp == 0:
        return 0
    return 1

print(get_ith_bit(3, 0))
print(get_ith_bit(3, 1))
print(get_ith_bit(3, 2))

Output is:
1
1
0
which is 110 in binary as we saw before as well.

Complete code

The complete code for the problem statement is as follows:
PS: Given an array of integers, "data" , find all possible subsets of data, including the empty set.
def get_ith_bit(num, i):
    # Shift the operand specified number of bits to the left
    temp = (1 << i)
    temp = temp & num
    if temp == 0:
        return 0
    return 1

def find_all_subsets(data):
    subsets = []

    if not data:
        return [[]]
    else:
        combinations = 2 ** len(data)
        for i in range(0, combinations):
            subset = set()
            for j in range(0, len(data)):
                if get_ith_bit(i, j) == 1 and data[j] not in subset:
                    subset.add(data[j])

            if i == 0:
                subsets.append([])
            else:
                subsets.append(list(subset))
    return subsets

print(find_all_subsets([2,5,7]))
Output:
[[], [2], [5], [2, 5], [7], [2, 7], [5, 7], [2, 5, 7]]


Related links

Sunday, 12 January 2025

Working with heaps in Python

 Background

As we all know a heap is a complete binary tree that satisfies the heap property:

  • Min heap: The value of children is greater than or equal to the parent node. This means the root node of a min heap stores the lowest value or minimum value data.
  • Max heap: The value of children is smaller than or equal to the parent. This means that the root node of a max heap stores the highest value or the maximum value data.

Heaps are used to implement priority queues for various use cases. See the below diagrams on what constitutes a valid and invalid heap (Credit: GeeksForGeeks)

Valid min heaps



Invalid min heaps


Valid max heaps


Invalid max heaps


It is an interesting read on how to insert in a heap, how to remove from the heap, how to heapify an array, how to use an array to represent a heap (The above diagrams show binary tree representation), how to use heaps for implementing priority queue etc. 

The time complexity for various heap operations is as follows:



Working with heaps in Python

Now that we brushed upon heap data structure above let's get to the original purpose of this post, which s to understand how to work with heaps in Python.

In Java, you would usually use PriorityQueue implementation to work with heaps. In Python we use python’s inbuilt library named heapq.

Look at the following code:

import heapq

data = [3, 5, 9, 14, 4, 24, 2]
print(f"Original data: {data}")
heapq.heapify(data) # Create min heap
print(f"After heapify: {data}")
heapq.heappush(data, 1)
print(f"After pushing to heap: {data}")
heapq.heappop(data)
print(f"After poping from heap: {data}")

It Prints:

Original data: [3, 5, 9, 14, 4, 24, 2]
After heapify: [2, 4, 3, 14, 5, 24, 9]
After pushing to heap: [1, 2, 3, 4, 5, 24, 9, 14]|
After poping from heap: [2, 4, 3, 14, 5, 24, 9]

As you can see we initially has a simple list called data which we then converted into a min heap (default heap behavior) using heapify method (O(N) time complexity). We then pushed an element to the min heap (O(Log(N)) time complexity) and then poped one which remoes the element from the root - minimum element in this case of min heap (Also has time complexity O(Log(N)))

NOTE: If you want max heap implementation you can just negate the data and push it in the heap.

NOTE: Heap elements can be tuples. This is useful for assigning comparison values (such as task priorities) alongside the main record being tracked. 1st element in the tuple is use for the comparison/priority.

See below code of how we can add tuple in the heap:

import heapq

data = []
heapq.heappush(data, (3, "China"))
heapq.heappush(data, (1, "India"))
heapq.heappush(data, (2, "USA"))
print(data)

It prints:

[(1, 'India'), (3, 'China'), (2, 'USA')]

Related Links

Saturday, 11 January 2025

Working with dictionaries in Python

 Background

In this post, we will see how we can use dictionaries and specifically how we can enable a custom class in Python to be used as a dict key. I am originally from a Java background and have written posts before on how HashMap/HashTable works in Java (See Related links section towards the end of this post). 

If someone is new
  • Dictionaries are data structures that store data of format key: value pairs
  • Data stored in dict is unique (does not allow duplicate keys), mutable (can edit), and ordered
NOTE: As of Python version 3.7, dictionaries are ordered. In Python 3.6 and earlier, dictionaries are unordered.

Dictionaries in Python

You can initialize a dictionary in Python using {} or dict keyword

  • my_dict = {}
  • my_dict_1 = {"Name": "Aniket", "Country": "India"}
  • my_dict_2 = dict(name="Aniket")
You can print these and see the dictionary. It will print
{}
{'Name': 'Aniket', 'Country': 'India'}
{'name': 'Aniket'}


A normal dict like above is unordered which means the order in which you insert data in dict is not maintained, when you iterate over items in dict it may give you a different order. This is the same as HashMap in Java. However, similar to LinkedHashMap we have OrderedDict in Python (Please note that as of Python version 3.7, dictionaries are ordered. In Python 3.6 and earlier, dictionaries are unordered).

You can also merge one map to another using the update method. Remember key's of a dict are unique and similar to HashMap if you try to insert a key that is already present in the dict then it is going to override the key data with the new value (See below code).
my_dict = {}
my_dict["name"] = "Aniket"
my_dict["Country"] = "India"
for key in my_dict:
    print(f"{key} : {my_dict[key]}")
print("--------------")
others_dict = {}
others_dict["name"] = "John"
others_dict["State"] = "NYC"
for key in others_dict:
    print(f"{key} : {others_dict[key]}")
print("--------------")
my_dict.update(others_dict)
for key in my_dict:
    print(f"{key} : {my_dict[key]}")


Output is:
name : Aniket
Country : India
--------------
name : John
State : NYC
--------------
name : John
Country : India
State : NYC


See how name key value was overridden by new dict key, Country key was not overridden so it stated the same and finally a new key called State was added.

Using a custom class as a key in dict

In Java for a new class, you would typically override equals and hashcode method to make it work, in Python we have to do something similar as well.

Let's create an Employee class with just name and age, try to create objects out of it, push to dict and print it.

class Employee:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Employee with name: {self.name} & age: {self.age}"


my_dict = {}
e1 = Employee("Aniket", 30)
e2 = Employee("Aniket", 30)
my_dict[e1] = 1
my_dict[e2] = 2
for key, value in my_dict.items():
    print(f"{key} : {value}")

Output is:
Employee with name: Aniket & age: 30 : 1
Employee with name: Aniket & age: 30 : 2


As you see from output it took the same key as different keys in dict and added separate entries. Since the employee (uniquely defined by name and age) is same we want just one entry in dict corresponding to a unique name and age. In this case it should have overridden value from 1 to 2 and just printed single entry with value 2. Let's see how we do that.


For the above to work similarly to Java (where we override equals and hashcode), in Python we override the two methods below
  • __eq__()
  • __hasg__()

So your code will now look like

class Employee:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Employee with name: {self.name} & age: {self.age}"

    def __eq__(self, other):
        return (self.name, self.age) == (other.name, other.age)

    def __hash__(self):
        return hash((self.name, self.age))

my_dict = {}
e1 = Employee("Aniket", 30)
e2 = Employee("Aniket", 30)
my_dict[e1] = 1
my_dict[e2] = 2
for key, value in my_dict.items():
    print(f"{key} : {value}")


This now prints:
Employee with name: Aniket & age: 30 : 2

 which is in line with our expectations.

See the below diagram for all methods supported by Python dict



Related Links

Wednesday, 8 January 2025

Different ways to iterate over a list in Python

 Background

In this post, we will see the different ways to iterate over a list in Python.

Different ways to iterate over a list in Python

Using for loop



A simple way to iterate over a list is to use a for loop.
  
names = ["A", "B", "C"]
for name in names:
    print(name)

This prints
A
B
C

Using for loop with range

You can also use a for loop with range if you want to access the list with it's index
  
names = ["A", "B", "C"]
for name in names:
    print(name)

This also prints
A
B
C

Using enumerate

If you want index and value both then you can use enumerate as follows
  
names = ["A", "B", "C"]
for idx, name in enumerate(names):
    print(f"{name} at index {idx}")

This prints
A at index 0
B at index 1
C at index 2

Using while loop

You can also use a while loop for iterating over a list as follows
  
names = ["A", "B", "C"]
i=0
while i<len(names):
    print(names[i])
    i = i + 1

This prints:
A
B
C

List Comprehension

List comprehension is a more concise way to iterate over a list.
  
names = ["A", "B", "C"]
[print(name) for name in names]
This prints:
A
B
C

If you print above list it will print [None, None, None] as you are not creating any new element for storing in new list on iterating the original list

NOTE: This creates a new list and is not a recommended way to iterate over a list. You can use this if you have a usecase to create a new list from existing one with filtering or modifying the original list.

Related Links

Tuesday, 7 January 2025

Slicing strings in Python

 Background

Working with string is very common in any programming language and what comes in handy in Python is slicing. In this post, we will try to understand how slicing works for strings in Python.

Slicing in Python

The syntax for slicing is : Object [start:stop:step] where
  • "start" specifies the starting index of a slice 
  • "stop" specifies the ending element of a slice (not included)
  • "step" specifies the number of elements to jump/step on between start and stop.
Slicing can be done with negative indexes as well and if start/stop is negative that the indexes are counted from the end backward (See diagram below to understand the indexes - positive & negative).

See a few examples below
  
text = "ABCDE"
print(text[0:3])
print(text[0:3:2])
print(text[-4:-1])
print(text[-4:-1:2])

This prints:
ABC
AC
BCD
BD

You can also have the step as a negative number which will essentially consider elements backwards (reversing the data structure). See the following examples (more details on using this to reverse data structure are added in a later section).
  
text = "ABCDE"
print(text[3:0:-1])
print(text[3:0:-2])
print(text[::-1])
print(text[-1:-5:-1])

This prints:
DCB
DB
EDCBA
EDCB

NOTE: text[::] or text[:] is going to create a copy of the existing data structure.

Reversing Elements of Data Structure

You can use a negative step to reverse the elements of the following data structures

  
# list
l = ["a", "b", "c"]
print(l)
print(l[::-1])

# string
s = "abc"
print(s)
print(s[::-1])

# tuple
t = ("a", "b", "c")
print(t)
print(t[::-1])


This prints:
['a', 'b', 'c']
['c', 'b', 'a']
abc
cba
('a', 'b', 'c')
('c', 'b', 'a')

Cheat Sheet

  • a[start:stop]  # items start through stop-1
  • a[start:]      # items start through the rest of the array
  • a[:stop]       # items from the beginning through stop-1
  • a[:]           # a copy of the whole array
We can also pass step to the slicing which determines how many steps to jump for slicing
  • a[start:stop:step] # start through not past stop, by step
Step can also be a negative number
  • a[::-1]    # all items in the array, reversed
  • a[1::-1]   # the first two items, reversed
  • a[:-3:-1]  # the last two items, reversed
  • a[-3::-1]  # everything except the last two items, reversed

Related Links

t> UA-39527780-1 back to top